[automerger skipped] DO NOT MERGE - Merge pi-dev@5234907 into stage-aosp-master
am: c8b794291c -s ours
am skip reason: subject contains skip directive

Change-Id: I65dd67f811e4c464cc8a60cbbb51ea349c544fba
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..4268636
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,96 @@
+//
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+version_name = "1.20-asop"
+version_code = "417000328"
+
+android_app {
+    name: "LiveTv",
+
+    srcs: ["src/**/*.java"],
+
+    // TODO(b/122608868) turn proguard back on
+    optimize: {
+        enabled: false,
+    },
+
+    // It is required for com.android.providers.tv.permission.ALL_EPG_DATA
+    privileged: true,
+
+    sdk_version: "system_current",
+    min_sdk_version: "23", // M
+
+    resource_dirs: [
+        "res",
+        "material_res",
+
+    ],
+
+    libs: ["tv-guava-android-jar"],
+
+    static_libs: [
+        "android-support-annotations",
+        "android-support-compat",
+        "android-support-core-ui",
+        "androidx.tvprovider_tvprovider",
+        "android-support-v4",
+        "android-support-v7-appcompat",
+        "android-support-v7-palette",
+        "android-support-v7-preference",
+        "android-support-v7-recyclerview",
+        "android-support-v14-preference",
+        "android-support-v17-leanback",
+        "android-support-v17-preference-leanback",
+        "jsr330",
+        "live-channels-partner-support",
+        "live-tv-tuner-proto",
+        "live-tv-tuner",
+        "tv-auto-value-jar",
+        "tv-auto-factory-jar",
+        "tv-common",
+        "tv-error-prone-annotations-jar",
+        "tv-lib-dagger",
+        "tv-lib-exoplayer",
+        "tv-lib-exoplayer-v2-core",
+        "tv-lib-dagger-android",
+    ],
+
+    plugins: [
+        "tv-auto-value",
+        "tv-auto-factory",
+        "tv-lib-dagger-android-processor",
+        "tv-lib-dagger-compiler",
+    ],
+
+    javacflags: [
+        "-Xlint:deprecation",
+        "-Xlint:unchecked",
+    ],
+
+    aaptflags: [
+        "--version-name",
+        version_name,
+
+        "--version-code",
+        version_code,
+
+        "--extra-packages",
+        "com.android.tv.tuner",
+
+        "--extra-packages",
+        "com.android.tv.common",
+    ],
+}
diff --git a/Android.mk b/Android.mk
deleted file mode 100644
index 351d8b5..0000000
--- a/Android.mk
+++ /dev/null
@@ -1,102 +0,0 @@
-#
-# Copyright (C) 2015 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-LOCAL_PATH:= $(call my-dir)
-
-include $(CLEAR_VARS)
-
-
-LOCAL_MODULE_TAGS := optional
-
-include $(LOCAL_PATH)/version.mk
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_PACKAGE_NAME := LiveTv
-
-# It is required for com.android.providers.tv.permission.ALL_EPG_DATA
-LOCAL_PRIVILEGED_MODULE := true
-
-LOCAL_SDK_VERSION := system_current
-LOCAL_MIN_SDK_VERSION := 23  # M
-
-LOCAL_USE_AAPT2 := true
-
-LOCAL_RESOURCE_DIR := \
-    $(LOCAL_PATH)/res \
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
-    android-support-annotations \
-    lib-exoplayer \
-    lib-exoplayer-v2-core \
-    jsr330 \
-
-LOCAL_STATIC_ANDROID_LIBRARIES := \
-    android-support-compat \
-    android-support-core-ui \
-    android-support-tv-provider \
-    android-support-v4 \
-    android-support-v7-appcompat \
-    android-support-v7-palette \
-    android-support-v7-preference \
-    android-support-v7-recyclerview \
-    android-support-v14-preference \
-    android-support-v17-leanback \
-    android-support-v17-preference-leanback \
-    live-channels-partner-support \
-    live-tv-tuner \
-    tv-common \
-
-
-LOCAL_JAVACFLAGS := -Xlint:deprecation -Xlint:unchecked
-
-LOCAL_AAPT_FLAGS += \
-    --version-name "$(version_name_package)" \
-    --version-code $(version_code_package) \
-
-LOCAL_JNI_SHARED_LIBRARIES := libtunertvinput_jni
-LOCAL_AAPT_FLAGS += --extra-packages com.android.tv.tuner
-
-include $(BUILD_PACKAGE)
-
-#############################################################
-# Pre-built dependency jars
-#############################################################
-prebuilts := \
-    lib-exoplayer:libs/exoplayer-r1.5.16.aar \
-    lib-exoplayer-v2-core:libs/exoplayer-core-2-SNAPHOT-20180114.aar \
-    auto-value-jar:../../../prebuilts/tools/common/m2/repository/com/google/auto/value/auto-value/1.5.2/auto-value-1.5.2.jar \
-    javax-annotations-jar:../../../prebuilts/tools/common/m2/repository/javax/annotation/javax.annotation-api/1.2/javax.annotation-api-1.2.jar \
-    truth-0-36-prebuilt-jar:../../../prebuilts/tools/common/m2/repository/com/google/truth/truth/0.36/truth-0.36.jar \
-
-define define-prebuilt
-  $(eval tw := $(subst :, ,$(strip $(1)))) \
-  $(eval include $(CLEAR_VARS)) \
-  $(eval LOCAL_MODULE := $(word 1,$(tw))) \
-  $(eval LOCAL_MODULE_TAGS := optional) \
-  $(eval LOCAL_MODULE_CLASS := JAVA_LIBRARIES) \
-  $(eval LOCAL_SRC_FILES := $(word 2,$(tw))) \
-  $(eval LOCAL_UNINSTALLABLE_MODULE := true) \
-  $(eval LOCAL_SDK_VERSION := current) \
-  $(eval include $(BUILD_PREBUILT))
-endef
-
-$(foreach p,$(prebuilts),\
-  $(call define-prebuilt,$(p)))
-
-prebuilts :=
-
-include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 3456f16..a398823 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -14,17 +14,19 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
 -->
+<!-- This manifest is for LiveTv -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:tools="http://schemas.android.com/tools"
     package="com.android.tv" >
 
     <uses-sdk
         android:minSdkVersion="23"
-        android:targetSdkVersion="26" />
+        android:targetSdkVersion="27" />
 
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
     <uses-permission android:name="android.permission.CHANGE_HDMI_CEC_ACTIVE_SOURCE" />
-    <uses-permission android:name="android.permission.GLOBAL_SEARCH" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
     <uses-permission android:name="android.permission.HDMI_CEC" />
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.MODIFY_PARENTAL_CONTROLS" />
@@ -32,6 +34,7 @@
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.READ_TV_LISTINGS" />
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" />
     <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
     <uses-permission android:name="com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA" />
@@ -70,22 +73,14 @@
     <application
         android:name="com.android.tv.app.LiveTvApplication"
         android:allowBackup="true"
-        android:banner="@drawable/banner"
-        android:icon="@drawable/ic_live_channels"
+        android:appComponentFactory="android.support.v4.app.CoreComponentFactory"
+        android:banner="@drawable/live_tv_banner"
+        android:icon="@drawable/ic_tv_app"
         android:label="@string/app_name"
         android:supportsRtl="true"
-        android:theme="@style/Theme.TV" >
-        <activity
-            android:name="com.android.tv.tuner.setup.LiveTvTunerSetupActivity"
-            android:configChanges="keyboard|keyboardHidden"
-            android:label="@string/bt_app_name"
-            android:launchMode="singleInstance"
-            android:process="com.android.tv.tuner"
-            android:theme="@style/Theme.Setup.GuidedStep" >
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-            </intent-filter>
-        </activity>
+        android:theme="@style/Theme.TV"
+        tools:replace="android:appComponentFactory">
+        >
 
         <!-- providers are listed here to keep them separate from the internal versions -->
         <provider
@@ -103,7 +98,32 @@
             android:exported="false"
             android:process="com.android.tv.common" />
 
-        <activity android:name="com.android.tv.TvActivity" >
+
+
+        <receiver
+            android:name="com.android.tv.livetv.receiver.GlobalKeyReceiver"
+            android:exported="true" >
+            <intent-filter>
+                <action android:name="android.intent.action.GLOBAL_BUTTON" />
+            </intent-filter>
+
+            <!--
+             Not directly related to GlobalKeyReceiver but needed to be able to provide our
+            content rating definitions to the system service.
+            -->
+            <intent-filter>
+                <action android:name="android.media.tv.action.QUERY_CONTENT_RATING_SYSTEMS" />
+            </intent-filter>
+
+            <meta-data
+                android:name="android.media.tv.metadata.CONTENT_RATING_SYSTEMS"
+                android:resource="@xml/tv_content_rating_systems" />
+        </receiver>
+
+        <activity
+            android:name="com.android.tv.TvActivity"
+            android:exported="true"
+            android:launchMode="singleTask" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
 
@@ -202,8 +222,9 @@
             </intent-filter>
         </activity>
         <activity
-            android:name="com.android.tv.dvr.ui.browse.DvrDetailsActivity"
+            android:name="com.android.tv.ui.DetailsActivity"
             android:configChanges="keyboard|keyboardHidden"
+            android:exported="true"
             android:theme="@style/Theme.TV.Dvr.Browse.Details" />
         <activity
             android:name="com.android.tv.dvr.ui.DvrSeriesSettingsActivity"
@@ -243,6 +264,7 @@
                 <action android:name="android.intent.action.PACKAGE_ADDED" />
                 <!-- PACKAGE_CHANGED for package enabled/disabled notification -->
                 <action android:name="android.intent.action.PACKAGE_CHANGED" />
+                <action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
                 <action android:name="android.intent.action.PACKAGE_REMOVED" />
 
                 <data android:scheme="package" />
@@ -255,7 +277,7 @@
             android:name="com.android.tv.setup.SystemSetupActivity"
             android:configChanges="keyboard|keyboardHidden"
             android:exported="true"
-            android:label="@string/bt_app_name"
+            android:label="@string/app_name"
             android:launchMode="singleInstance"
             android:theme="@style/Theme.Setup.GuidedStep" >
             <intent-filter>
@@ -263,23 +285,7 @@
 
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
-        </activity>
-        <!--
-          TunerInputController should be the same process with MainActivity to check status
-          of MainActivity
-        -->
-        <receiver
-            android:name="com.android.tv.tuner.TunerInputController$IntentReceiver"
-            android:exported="false" >
-            <intent-filter>
-                <action android:name="android.intent.action.BOOT_COMPLETED" />
-                <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
-                <action android:name="android.hardware.usb.action.USB_DEVICE_DETACHED" />
-                <action android:name="com.android.tv.action.APPLICATION_FIRST_LAUNCHED" />
-                <action android:name="com.android.tv.action.NETWORK_TUNER_ATTACHED" />
-                <action android:name="com.android.tv.action.NETWORK_TUNER_DETACHED" />
-            </intent-filter>
-        </receiver> <!-- DVR -->
+        </activity> <!-- DVR -->
         <service
             android:name="com.android.tv.dvr.recorder.DvrRecordingService"
             android:label="@string/dvr_service_name" />
@@ -287,48 +293,8 @@
         <receiver android:name="com.android.tv.dvr.recorder.DvrStartRecordingReceiver" />
 
         <service
-            android:name="com.android.tv.tuner.tvinput.TunerStorageCleanUpService"
-            android:exported="false"
-            android:permission="android.permission.BIND_JOB_SERVICE"
-            android:process="com.android.tv.tuner" />
-        <service
             android:name="com.android.tv.data.epg.EpgFetchService"
             android:permission="android.permission.BIND_JOB_SERVICE" />
-
-        <receiver
-            android:name="com.android.tv.livetv.receiver.GlobalKeyReceiver"
-            android:exported="true" >
-            <intent-filter>
-                <action android:name="android.intent.action.GLOBAL_BUTTON" />
-            </intent-filter>
-
-            <!--
-             Not directly related to GlobalKeyReceiver but needed to be able to provide our
-            content rating definitions to the system service.
-            -->
-            <intent-filter>
-                <action android:name="android.media.tv.action.QUERY_CONTENT_RATING_SYSTEMS" />
-            </intent-filter>
-
-            <meta-data
-                android:name="android.media.tv.metadata.CONTENT_RATING_SYSTEMS"
-                android:resource="@xml/tv_content_rating_systems" />
-        </receiver>
-
-        <service
-            android:name="com.android.tv.tuner.livetuner.LiveTvTunerTvInputService"
-            android:enabled="false"
-            android:label="@string/bt_app_name"
-            android:permission="android.permission.BIND_TV_INPUT"
-            android:process="com.android.tv.tuner" >
-            <intent-filter>
-                <action android:name="android.media.tv.TvInputService" />
-            </intent-filter>
-
-            <meta-data
-                android:name="android.media.tv.input"
-                android:resource="@xml/ut_tvinputservice" />
-        </service>
     </application>
 
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..63c1f44
--- /dev/null
+++ b/README.md
@@ -0,0 +1,36 @@
+# Live TV
+
+__Live TV__ is the Open Source reference application for watching TV on Android TVs.
+
+## Source
+
+The source of truth is an internal google repository (aka google3) at
+cs/third_party/java_src/android_app/live_channels
+
+Changes are made in the google3 repository and automatically pushed here.
+
+The following files are only in the android repository and must be changed there.
+
+* *.mk
+* \*\*/lib/\*.\*
+
+## AOSP instructions
+
+To install LiveTv
+
+```bash
+echo "Compiling"
+m -j LiveTv
+echo  "Installing"
+adb install -r ${OUT}/system/priv-app/LiveTv/LiveTv.apk
+
+```
+
+If it is your first time installing LiveTv you will need to do
+
+```bash
+adb root
+adb remount
+adb push ${OUT}/system/priv-app/LiveTv/LiveTv.apk /system/priv-app/LiveTv/LiveTv.apk
+adb reboot
+```
\ No newline at end of file
diff --git a/ResourceManifest.xml b/ResourceManifest.xml
deleted file mode 100644
index a859327..0000000
--- a/ResourceManifest.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2018 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.tv" xmlns:tools="http://schemas.android.com/tools">
-    <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="23"/>
-    <application />
-</manifest>
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..23e3dbd
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ * Experimental gradle configuration.  This file may not be up to date.
+ */
+
+buildscript {
+    repositories {
+        mavenCentral()
+        google()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.1.4'
+        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.6'
+    }
+}
+apply plugin: 'com.android.application'
+android {
+    compileSdkVersion 28
+    buildToolsVersion '28.0.3'
+    dexOptions {
+        preDexLibraries = false
+        additionalParameters=['--core-library']
+        javaMaxHeapSize "6g"
+    }
+    android {
+        defaultConfig {
+            resConfigs "en"
+        }
+    }
+    defaultConfig {
+        minSdkVersion 23
+        targetSdkVersion 28
+        versionCode 1
+        versionName "1.0"
+    }
+    buildTypes {
+        debug {
+            minifyEnabled false
+        }
+        release {
+            minifyEnabled true
+        }
+    }
+    compileOptions() {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+    sourceSets {
+        main {
+            res.srcDirs = ['res', 'material_res']
+            java.srcDirs = ['src', 'partner_support/src']
+            manifest.srcFile 'AndroidManifest.xml'
+        }
+    }
+}
+
+repositories {
+    mavenCentral()
+    jcenter()
+    google()
+}
+
+dependencies {
+    implementation 'androidx.appcompat:appcompat:1.0.2'
+    implementation 'androidx.palette:palette:1.0.0'
+    implementation 'androidx.leanback:leanback:1.0.0'
+    implementation "androidx.tvprovider:tvprovider:1.0.0"
+    implementation "androidx.recyclerview:recyclerview:1.0.0"
+    implementation "androidx.recyclerview:recyclerview-selection:1.0.0"
+    implementation "androidx.palette:palette:1.0.0"
+
+    implementation 'com.google.dagger:dagger:2.18'
+    implementation 'com.google.dagger:dagger-android:2.18'
+    annotationProcessor 'com.google.dagger:dagger-compiler:2.18'
+    annotationProcessor 'com.google.dagger:dagger-android-processor:2.18'
+
+    /*Not building with  latest one (1.6.3)*/
+    annotationProcessor 'com.google.auto.value:auto-value:1.5.4'
+    implementation 'com.google.auto.value:auto-value:1.5.4'
+    implementation 'javax.inject:javax.inject:1'
+    implementation 'com.google.guava:guava:26.0-android'
+    implementation project(':common')
+}
\ No newline at end of file
diff --git a/common/Android.bp b/common/Android.bp
new file mode 100644
index 0000000..63759d4
--- /dev/null
+++ b/common/Android.bp
@@ -0,0 +1,61 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+android_library {
+    name: "tv-common",
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.proto",
+    ],
+
+    sdk_version: "system_current",
+
+    proto: {
+        type: "lite",
+    },
+
+    resource_dirs: ["res"],
+
+    libs: [
+        "tv-auto-value-jar",
+        "tv-auto-factory-jar",
+        "android-support-annotations",
+        "tv-error-prone-annotations-jar",
+        "tv-guava-android-jar",
+        "jsr330",
+        "tv-lib-dagger",
+        "tv-lib-exoplayer",
+        "tv-lib-exoplayer-v2-core",
+        "android-support-compat",
+        "android-support-core-ui",
+        "android-support-v7-recyclerview",
+        "android-support-v17-leanback",
+    ],
+
+    static_libs: ["tv-lib-dagger-android"],
+
+    plugins: [
+        "tv-auto-value",
+        "tv-auto-factory",
+        "tv-lib-dagger-android-processor",
+        "tv-lib-dagger-compiler",
+    ],
+
+
+    min_sdk_version: "23",
+
+    // TODO(b/77284273): generate build config after dagger supports libraries
+    //include $(LOCAL_PATH)/buildconfig.mk
+
+}
diff --git a/common/Android.mk b/common/Android.mk
deleted file mode 100644
index 48f969e..0000000
--- a/common/Android.mk
+++ /dev/null
@@ -1,31 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-# Include all common java files.
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_MODULE := tv-common
-LOCAL_MODULE_CLASS := STATIC_JAVA_LIBRARIES
-LOCAL_MODULE_TAGS := optional
-LOCAL_SDK_VERSION := system_current
-
-LOCAL_USE_AAPT2 := true
-
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-
-LOCAL_JAVA_LIBRARIES := \
-    android-support-annotations
-
-LOCAL_DISABLE_RESOLVE_SUPPORT_LIBRARIES := true
-
-LOCAL_SHARED_ANDROID_LIBRARIES := \
-    android-support-compat \
-    android-support-core-ui \
-    android-support-v7-recyclerview \
-    android-support-v17-leanback
-
-LOCAL_MIN_SDK_VERSION := 23
-
-include $(LOCAL_PATH)/buildconfig.mk
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/common/AndroidManifest.xml b/common/AndroidManifest.xml
index c1c698c..7002d5f 100644
--- a/common/AndroidManifest.xml
+++ b/common/AndroidManifest.xml
@@ -17,6 +17,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.tv.common"
           android:versionCode="1">
-    <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="21"/>
+    <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
     <application />
 </manifest>
diff --git a/common/build.gradle b/common/build.gradle
new file mode 100644
index 0000000..f371475
--- /dev/null
+++ b/common/build.gradle
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ * Experimental gradle configuration.  This file may not be up to date.
+ */
+
+apply plugin: 'com.android.library'
+apply plugin: 'com.google.protobuf'
+buildscript {
+    repositories {
+        mavenCentral()
+        google()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.1.4'
+        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.6'
+    }
+}
+android {
+    compileSdkVersion 28
+    buildToolsVersion '28.0.3'
+    dexOptions {
+        preDexLibraries = false
+        additionalParameters = ['--core-library']
+        javaMaxHeapSize "6g"
+    }
+
+    android {
+        defaultConfig {
+            resConfigs "en"
+        }
+    }
+
+    defaultConfig {
+        minSdkVersion 23
+        targetSdkVersion 28
+        versionCode 1
+        versionName "1.0"
+    }
+    buildTypes {
+        debug {
+            minifyEnabled false
+            buildConfigField "boolean", "AOSP", "true"
+            buildConfigField "boolean", "ENG", "true"
+            buildConfigField "boolean", "NO_JNI_TEST", "false"
+        }
+        release {
+            minifyEnabled true
+            buildConfigField "boolean", "AOSP", "true"
+            buildConfigField "boolean", "ENG", "false"
+            buildConfigField "boolean", "NO_JNI_TEST", "false"
+        }
+    }
+    compileOptions() {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+
+    sourceSets {
+        main {
+            res.srcDirs = ['res']
+            java.srcDirs = ['src']
+            manifest.srcFile 'AndroidManifest.xml'
+            proto {
+                srcDir 'src/com/android/tv/common/compat/internal'
+            }
+        }
+    }
+}
+
+repositories {
+    mavenCentral()
+    google()
+}
+
+dependencies {
+    implementation 'androidx.appcompat:appcompat:1.0.2'
+    implementation 'androidx.palette:palette:1.0.0'
+    implementation 'androidx.leanback:leanback:1.0.0'
+    implementation "androidx.tvprovider:tvprovider:1.0.0"
+    implementation "androidx.recyclerview:recyclerview:1.0.0"
+    implementation "androidx.recyclerview:recyclerview-selection:1.0.0"
+    implementation "androidx.palette:palette:1.0.0"
+    implementation 'com.google.guava:guava:26.0-android'
+    implementation 'com.google.protobuf:protobuf-java:3.0.0'
+    implementation 'com.google.dagger:dagger:2.18'
+    implementation 'com.google.dagger:dagger-android:2.18'
+    annotationProcessor 'com.google.dagger:dagger-compiler:2.18'
+    annotationProcessor 'com.google.dagger:dagger-android-processor:2.18'
+}
+protobuf {
+    // Configure the protoc executable
+    protoc {
+        artifact = 'com.google.protobuf:protoc:3.0.0'
+
+        plugins {
+            javalite {
+                // The codegen for lite comes as a separate artifact
+                artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0'
+            }
+        }
+
+        generateProtoTasks {
+            all().each {
+                task -> task.builtins {
+                    remove java
+                }
+                task.plugins {
+                    javalite {}
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/common/src/com/android/tv/common/BaseApplication.java b/common/src/com/android/tv/common/BaseApplication.java
index 71c9b4d..45c3256 100644
--- a/common/src/com/android/tv/common/BaseApplication.java
+++ b/common/src/com/android/tv/common/BaseApplication.java
@@ -17,10 +17,7 @@
 package com.android.tv.common;
 
 import android.annotation.TargetApi;
-import android.app.Application;
 import android.content.Context;
-import android.content.Intent;
-import android.os.AsyncTask;
 import android.os.Build;
 import android.os.StrictMode;
 import android.support.annotation.VisibleForTesting;
@@ -30,9 +27,10 @@
 import com.android.tv.common.util.CommonUtils;
 import com.android.tv.common.util.Debug;
 import com.android.tv.common.util.SystemProperties;
+import dagger.android.DaggerApplication;
 
 /** The base application class for Live TV applications. */
-public abstract class BaseApplication extends Application implements BaseSingletons {
+public abstract class BaseApplication extends DaggerApplication implements BaseSingletons {
     private RecordingStorageStatusManager mRecordingStorageStatusManager;
 
     /**
@@ -41,7 +39,13 @@
      */
     @VisibleForTesting public static BaseSingletons sSingletons;
 
-    /** Returns the {@link BaseSingletons} using the application context. */
+    /**
+     * Returns the {@link BaseSingletons} using the application context.
+     *
+     * @deprecated use {@link com.android.tv.common.singletons.HasSingletons#get(Class, Context)}
+     *     instead
+     */
+    @Deprecated
     public static BaseSingletons getSingletons(Context context) {
         // STOP-SHIP: changing the method to protected once the Tuner application is created.
         // No need to be "synchronized" because this doesn't create any instance.
@@ -65,8 +69,15 @@
             StrictMode.ThreadPolicy.Builder threadPolicyBuilder =
                     new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog();
             // TODO(b/69565157): Turn penaltyDeath on for VMPolicy when tests are fixed.
+            // TODO(b/120840665): Restore detecting untagged network sockets
             StrictMode.VmPolicy.Builder vmPolicyBuilder =
-                    new StrictMode.VmPolicy.Builder().detectAll().penaltyLog();
+                    new StrictMode.VmPolicy.Builder()
+                            .detectActivityLeaks()
+                            .detectLeakedClosableObjects()
+                            .detectLeakedRegistrationObjects()
+                            .detectFileUriExposure()
+                            .detectContentUriWithoutPermission()
+                            .penaltyLog();
 
             if (!CommonUtils.isRunningInTest()) {
                 threadPolicyBuilder.penaltyDialog();
@@ -77,14 +88,6 @@
         if (CommonFeatures.DVR.isEnabled(this)) {
             getRecordingStorageStatusManager();
         }
-        new AsyncTask<Void, Void, Void>() {
-            @Override
-            protected Void doInBackground(Void... params) {
-                // Fetch remote config
-                getRemoteConfig().fetch(null);
-                return null;
-            }
-        }.execute();
     }
 
     @Override
@@ -101,7 +104,4 @@
         }
         return mRecordingStorageStatusManager;
     }
-
-    @Override
-    public abstract Intent getTunerSetupIntent(Context context);
 }
diff --git a/common/src/com/android/tv/common/BaseSingletons.java b/common/src/com/android/tv/common/BaseSingletons.java
index e735cdb..1053061 100644
--- a/common/src/com/android/tv/common/BaseSingletons.java
+++ b/common/src/com/android/tv/common/BaseSingletons.java
@@ -16,20 +16,17 @@
 
 package com.android.tv.common;
 
-import android.content.Context;
-import android.content.Intent;
-import com.android.tv.common.config.api.RemoteConfig.HasRemoteConfig;
+import com.android.tv.common.buildtype.HasBuildType;
+import com.android.tv.common.flags.has.HasCloudEpgFlags;
+import com.android.tv.common.flags.has.HasConcurrentDvrPlaybackFlags;
 import com.android.tv.common.recording.RecordingStorageStatusManager;
 import com.android.tv.common.util.Clock;
 
 /** Injection point for the base app */
-public interface BaseSingletons extends HasRemoteConfig {
+public interface BaseSingletons
+        extends HasCloudEpgFlags, HasBuildType, HasConcurrentDvrPlaybackFlags {
 
     Clock getClock();
 
     RecordingStorageStatusManager getRecordingStorageStatusManager();
-
-    Intent getTunerSetupIntent(Context context);
-
-    String getEmbeddedTunerInputId();
 }
diff --git a/src/com/android/tv/util/Filter.java b/common/src/com/android/tv/common/BuildConfig.java
similarity index 60%
copy from src/com/android/tv/util/Filter.java
copy to common/src/com/android/tv/common/BuildConfig.java
index 3e24a49..b3ad002 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/common/src/com/android/tv/common/BuildConfig.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2019 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,11 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package com.android.tv.common;
 
-package com.android.tv.util;
+/* Hard coded BuildConfig. */
+public final class BuildConfig {
+    public static final boolean DEBUG = true;
+    public static final boolean ENG = false;
+    public static final boolean NO_JNI_TEST = false;
+    public static final boolean AOSP = true;
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
-}
+    private BuildConfig() {}
+}
\ No newline at end of file
diff --git a/common/src/com/android/tv/common/CommonConstants.java b/common/src/com/android/tv/common/CommonConstants.java
index ac379d1..43d9851 100644
--- a/common/src/com/android/tv/common/CommonConstants.java
+++ b/common/src/com/android/tv/common/CommonConstants.java
@@ -19,10 +19,20 @@
 /** Constants for common use in apps and tests. */
 public final class CommonConstants {
 
+    @Deprecated // TODO(b/121158110) refactor so this is not needed.
     public static final String BASE_PACKAGE =
+// AOSP_Comment_Out             !BuildConfig.AOSP
+// AOSP_Comment_Out                     ? "com.google.android.tv"
+// AOSP_Comment_Out                     :
                     "com.android.tv";
     /** A constant for the key of the extra data for the app linking intent. */
     public static final String EXTRA_APP_LINK_CHANNEL_URI = "app_link_channel_uri";
 
+    /**
+     * Video is unavailable because the source is not physically connected, for example the HDMI
+     * cable is not connected.
+     */
+    public static final int VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED = 5;
+
     private CommonConstants() {}
 }
diff --git a/common/src/com/android/tv/common/TvContentRatingCache.java b/common/src/com/android/tv/common/TvContentRatingCache.java
index cfdb8e4..f2fda69 100644
--- a/common/src/com/android/tv/common/TvContentRatingCache.java
+++ b/common/src/com/android/tv/common/TvContentRatingCache.java
@@ -23,9 +23,8 @@
 import android.util.ArrayMap;
 import android.util.Log;
 import com.android.tv.common.memory.MemoryManageable;
-import java.util.ArrayList;
+import com.google.common.collect.ImmutableList;
 import java.util.Collections;
-import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.SortedSet;
@@ -42,19 +41,19 @@
     }
 
     // @GuardedBy("TvContentRatingCache.this")
-    private final Map<String, TvContentRating[]> mRatingsMultiMap = new ArrayMap<>();
+    private final Map<String, ImmutableList<TvContentRating>> mRatingsMultiMap = new ArrayMap<>();
 
     /**
      * Returns an array TvContentRatings from a string of comma separated set of rating strings
-     * creating each from {@link TvContentRating#unflattenFromString(String)} if needed. Returns
-     * {@code null} if the string is empty or contains no valid ratings.
+     * creating each from {@link TvContentRating#unflattenFromString(String)} if needed or an empty
+     * list if the string is empty or contains no valid ratings.
      */
-    @Nullable
-    public synchronized TvContentRating[] getRatings(String commaSeparatedRatings) {
+    public synchronized ImmutableList<TvContentRating> getRatings(
+            @Nullable String commaSeparatedRatings) {
         if (TextUtils.isEmpty(commaSeparatedRatings)) {
-            return null;
+            return ImmutableList.of();
         }
-        TvContentRating[] tvContentRatings;
+        ImmutableList<TvContentRating> tvContentRatings;
         if (mRatingsMultiMap.containsKey(commaSeparatedRatings)) {
             tvContentRatings = mRatingsMultiMap.get(commaSeparatedRatings);
         } else {
@@ -76,12 +75,13 @@
 
     /** Returns a sorted array of TvContentRatings from a comma separated string of ratings. */
     @VisibleForTesting
-    static TvContentRating[] stringToContentRatings(String commaSeparatedRatings) {
+    static ImmutableList<TvContentRating> stringToContentRatings(
+            @Nullable String commaSeparatedRatings) {
         if (TextUtils.isEmpty(commaSeparatedRatings)) {
-            return null;
+            return ImmutableList.of();
         }
         Set<String> ratingStrings = getSortedSetFromCsv(commaSeparatedRatings);
-        List<TvContentRating> contentRatings = new ArrayList<>();
+        ImmutableList.Builder<TvContentRating> contentRatings = ImmutableList.builder();
         for (String rating : ratingStrings) {
             try {
                 contentRatings.add(TvContentRating.unflattenFromString(rating));
@@ -89,9 +89,7 @@
                 Log.e(TAG, "Can't parse the content rating: '" + rating + "'", e);
             }
         }
-        return contentRatings.size() == 0
-                ? null
-                : contentRatings.toArray(new TvContentRating[contentRatings.size()]);
+        return contentRatings.build();
     }
 
     private static Set<String> getSortedSetFromCsv(String commaSeparatedRatings) {
@@ -118,19 +116,17 @@
      * Returns a string of each flattened content rating, sorted and concatenated together with a
      * comma.
      */
-    public static String contentRatingsToString(TvContentRating[] contentRatings) {
-        if (contentRatings == null || contentRatings.length == 0) {
+    @Nullable
+    public static String contentRatingsToString(
+            @Nullable ImmutableList<TvContentRating> contentRatings) {
+        if (contentRatings == null) {
             return null;
         }
-        String[] ratingStrings = new String[contentRatings.length];
-        for (int i = 0; i < contentRatings.length; i++) {
-            ratingStrings[i] = contentRatings[i].flattenToString();
+        SortedSet<String> ratingStrings = new TreeSet<>();
+        for (TvContentRating rating : contentRatings) {
+            ratingStrings.add(rating.flattenToString());
         }
-        if (ratingStrings.length == 1) {
-            return ratingStrings[0];
-        } else {
-            return TextUtils.join(",", toSortedSet(ratingStrings));
-        }
+        return TextUtils.join(",", ratingStrings);
     }
 
     @Override
diff --git a/src/com/android/tv/util/Filter.java b/common/src/com/android/tv/common/buildtype/AospBuildTypeProvider.java
similarity index 65%
copy from src/com/android/tv/util/Filter.java
copy to common/src/com/android/tv/common/buildtype/AospBuildTypeProvider.java
index 3e24a49..8d39b3a 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/common/src/com/android/tv/common/buildtype/AospBuildTypeProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,11 +13,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package com.android.tv.common.buildtype;
 
-package com.android.tv.util;
+/** {@code AOSP} {@link HasBuildType}. */
+public class AospBuildTypeProvider implements HasBuildType {
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
+    @Override
+    public BuildType getBuildType() {
+        return BuildType.AOSP;
+    }
 }
diff --git a/src/com/android/tv/util/Filter.java b/common/src/com/android/tv/common/buildtype/EngBuildTypeProvider.java
similarity index 66%
copy from src/com/android/tv/util/Filter.java
copy to common/src/com/android/tv/common/buildtype/EngBuildTypeProvider.java
index 3e24a49..5f18794 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/common/src/com/android/tv/common/buildtype/EngBuildTypeProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,11 +13,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package com.android.tv.common.buildtype;
 
-package com.android.tv.util;
+/** {@code ENG} {@link HasBuildType}. */
+public class EngBuildTypeProvider implements HasBuildType {
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
+    @Override
+    public BuildType getBuildType() {
+        return BuildType.ENG;
+    }
 }
diff --git a/common/src/com/android/tv/common/buildtype/HasBuildType.java b/common/src/com/android/tv/common/buildtype/HasBuildType.java
new file mode 100644
index 0000000..7d5677c
--- /dev/null
+++ b/common/src/com/android/tv/common/buildtype/HasBuildType.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.buildtype;
+
+/**
+ * The provides a {@link BuildType} for selecting features in code.
+ *
+ * <p>This is considered an anti-pattern and new usages should be discouraged.
+ */
+public interface HasBuildType {
+
+    /** Compile time constant for build type. */
+    enum BuildType {
+        AOSP,
+        ENG,
+        NO_JNI_TEST,
+        PROD
+    }
+
+    BuildType getBuildType();
+}
diff --git a/src/com/android/tv/util/Filter.java b/common/src/com/android/tv/common/buildtype/NoJniTestBuildTypeProvider.java
similarity index 64%
copy from src/com/android/tv/util/Filter.java
copy to common/src/com/android/tv/common/buildtype/NoJniTestBuildTypeProvider.java
index 3e24a49..1620af2 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/common/src/com/android/tv/common/buildtype/NoJniTestBuildTypeProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,11 +13,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package com.android.tv.common.buildtype;
 
-package com.android.tv.util;
+/** {@code NO_JNI_TEST} {@link HasBuildType}. */
+public class NoJniTestBuildTypeProvider implements HasBuildType {
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
+    @Override
+    public BuildType getBuildType() {
+        return BuildType.NO_JNI_TEST;
+    }
 }
diff --git a/src/com/android/tv/util/Filter.java b/common/src/com/android/tv/common/buildtype/ProdBuildTypeProvider.java
similarity index 65%
copy from src/com/android/tv/util/Filter.java
copy to common/src/com/android/tv/common/buildtype/ProdBuildTypeProvider.java
index 3e24a49..16db326 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/common/src/com/android/tv/common/buildtype/ProdBuildTypeProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,11 +13,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package com.android.tv.common.buildtype;
 
-package com.android.tv.util;
+/** {@code Prod} {@link HasBuildType}. */
+public class ProdBuildTypeProvider implements HasBuildType {
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
+    @Override
+    public BuildType getBuildType() {
+        return BuildType.PROD;
+    }
 }
diff --git a/common/src/com/android/tv/common/compat/README.md b/common/src/com/android/tv/common/compat/README.md
new file mode 100644
index 0000000..8d87d83
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/README.md
@@ -0,0 +1,7 @@
+# TIF Compatibility Library
+
+This is the temporary location for the of Compatibility Library while it is under development.
+It will eventually move to a support library location.
+
+
+See go/tif-compat-proposal
\ No newline at end of file
diff --git a/common/src/com/android/tv/common/compat/RecordingSessionCompat.java b/common/src/com/android/tv/common/compat/RecordingSessionCompat.java
new file mode 100644
index 0000000..6941e47
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/RecordingSessionCompat.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.common.compat;
+
+import android.content.Context;
+import android.media.tv.TvInputService.RecordingSession;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.support.annotation.RequiresApi;
+import com.android.tv.common.compat.api.RecordingSessionCompatCommands;
+import com.android.tv.common.compat.api.RecordingSessionCompatEvents;
+import com.android.tv.common.compat.api.SessionEventNotifier;
+import com.android.tv.common.compat.internal.RecordingSessionCompatProcessor;
+
+/**
+ * TIF Compatibility for {@link RecordingSession}.
+ *
+ * <p>Extends {@code RecordingSession} in a backwards compatible way.
+ */
+@RequiresApi(api = VERSION_CODES.N)
+public abstract class RecordingSessionCompat extends RecordingSession
+        implements SessionEventNotifier,
+                RecordingSessionCompatCommands,
+                RecordingSessionCompatEvents {
+
+    private final RecordingSessionCompatProcessor mProcessor;
+
+    public RecordingSessionCompat(Context context) {
+        super(context);
+        mProcessor = new RecordingSessionCompatProcessor(this, this);
+    }
+
+    @Override
+    public void onAppPrivateCommand(String action, Bundle data) {
+        if (!mProcessor.handleAppPrivateCommand(action, data)) {
+            super.onAppPrivateCommand(action, data);
+        }
+    }
+
+    /** Display a debug message to the session for display on dev builds only */
+    @Override
+    public void onDevMessage(String message) {}
+
+    /** Notify the client to Display a message in the application as a toast on dev builds only. */
+    @Override
+    public void notifyDevToast(String message) {
+        mProcessor.notifyDevToast(message);
+    }
+
+    /** Notify the client Recording started. */
+    @Override
+    public void notifyRecordingStarted(String uri) {
+        mProcessor.notifyRecordingStarted(uri);
+    }
+}
diff --git a/common/src/com/android/tv/common/compat/TisSessionCompat.java b/common/src/com/android/tv/common/compat/TisSessionCompat.java
new file mode 100644
index 0000000..97f4fb3
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/TisSessionCompat.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.common.compat;
+
+import android.content.Context;
+import android.media.tv.TvInputService.Session;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.support.annotation.RequiresApi;
+import com.android.tv.common.compat.api.SessionCompatCommands;
+import com.android.tv.common.compat.api.SessionCompatEvents;
+import com.android.tv.common.compat.api.SessionEventNotifier;
+import com.android.tv.common.compat.internal.TifSessionCompatProcessor;
+
+/**
+ * TIF Compatibility for {@link Session}.
+ *
+ * <p>Extends {@code Session} in a backwards compatible way.
+ */
+@RequiresApi(api = VERSION_CODES.LOLLIPOP)
+public abstract class TisSessionCompat extends Session
+        implements SessionEventNotifier, SessionCompatCommands, SessionCompatEvents {
+
+    private final TifSessionCompatProcessor mTifCompatProcessor;
+
+    public TisSessionCompat(Context context) {
+        super(context);
+        mTifCompatProcessor = new TifSessionCompatProcessor(this, this);
+    }
+
+    @Override
+    public void onAppPrivateCommand(String action, Bundle data) {
+        if (!mTifCompatProcessor.handleAppPrivateCommand(action, data)) {
+            super.onAppPrivateCommand(action, data);
+        }
+    }
+
+    @Override
+    public void onDevMessage(String message) {}
+
+    @Override
+    public void notifyDevToast(String message) {
+        mTifCompatProcessor.notifyDevToast(message);
+    }
+
+    /**
+     * Notify the application with current signal strength.
+     *
+     * <p>At each {MainActivity#tune(boolean)}, the signal strength is implicitly reset to {@link
+     * TvInputConstantCompat#SIGNAL_STRENGTH_NOT_USED}. If a TV input supports reporting signal
+     * strength, it should set the signal strength to {@link
+     * TvInputConstantCompat#SIGNAL_STRENGTH_UNKNOWN} in
+     * {TunerSessionWorker#prepareTune(TunerChannel, String)}, until a valid strength is available.
+     *
+     * @param value The current signal strength. Valid values are {@link
+     *     TvInputConstantCompat#SIGNAL_STRENGTH_NOT_USED}, {@link
+     *     TvInputConstantCompat#SIGNAL_STRENGTH_UNKNOWN}, and 0 - 100 inclusive.
+     */
+    @Override
+    public void notifySignalStrength(int value) {
+        mTifCompatProcessor.notifySignalStrength(value);
+    }
+}
diff --git a/common/src/com/android/tv/common/compat/TvInputConstantCompat.java b/common/src/com/android/tv/common/compat/TvInputConstantCompat.java
new file mode 100644
index 0000000..251e848
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/TvInputConstantCompat.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat;
+
+/** Temp TIF Compatibility for {@link TvInputManager} constants. */
+public class TvInputConstantCompat {
+
+    /**
+     * Status for {@link TisSessionCompat#notifySignalStrength(int)} and
+     * {@link TvViewCompat.TvInputCallback#onTimeShiftStatusChanged(String, int)}:
+     *
+     * <p>SIGNAL_STRENGTH_NOT_USED means the TV Input does not report signal strength. Each onTune
+     * command implicitly resets the TV App's signal strength state to SIGNAL_STRENGTH_NOT_USED.
+     */
+    public static final int SIGNAL_STRENGTH_NOT_USED = -3;
+
+    /**
+     * Status for {@link TisSessionCompat#notifySignalStrength(int)} and
+     * {@link TvViewCompat.TvInputCallback#onTimeShiftStatusChanged(String, int)}:
+     *
+     * <p>SIGNAL_STRENGTH_ERROR means exception/error when handling signal strength.
+     */
+    public static final int SIGNAL_STRENGTH_ERROR = -2;
+
+    /**
+     * Status for {@link TisSessionCompat#notifySignalStrength(int)} and
+     * {@link TvViewCompat.TvInputCallback#onTimeShiftStatusChanged(String, int)}:
+     *
+     * <p>SIGNAL_STRENGTH_UNKNOWN means the TV Input supports signal strength, but does not
+     * currently know what the strength is.
+     */
+    public static final int SIGNAL_STRENGTH_UNKNOWN = -1;
+}
diff --git a/common/src/com/android/tv/common/compat/TvInputInfoCompat.java b/common/src/com/android/tv/common/compat/TvInputInfoCompat.java
new file mode 100644
index 0000000..685a3ed
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/TvInputInfoCompat.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.media.tv.TvInputInfo;
+import android.media.tv.TvInputService;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * TIF Compatibility for {@link TvInputInfo}.
+ */
+public class TvInputInfoCompat {
+    private static final String TAG = "TvInputInfoCompat";
+    private static final String ATTRIBUTE_NAMESPACE_ANDROID =
+            "http://schemas.android.com/apk/res/android";
+    private static final String TV_INPUT_XML_START_TAG_NAME = "tv-input";
+    private static final String TV_INPUT_EXTRA_XML_START_TAG_NAME = "extra";
+    private static final String ATTRIBUTE_NAME = "name";
+    private static final String ATTRIBUTE_VALUE = "value";
+    private static final String ATTRIBUTE_NAME_AUDIO_ONLY =
+            "com.android.tv.common.compat.tvinputinfocompat.audioOnly";
+
+    private final Context mContext;
+    private final TvInputInfo mTvInputInfo;
+    private final boolean mAudioOnly;
+
+    public TvInputInfoCompat(Context context, TvInputInfo tvInputInfo) {
+        mContext = context;
+        mTvInputInfo = tvInputInfo;
+        // TODO(b/112938832): use tvInputInfo.isAudioOnly() when SDK is updated
+        mAudioOnly = Boolean.parseBoolean(getExtras().get(ATTRIBUTE_NAME_AUDIO_ONLY));
+    }
+
+    public TvInputInfo getTvInputInfo() {
+        return mTvInputInfo;
+    }
+
+    public boolean isAudioOnly() {
+        return mAudioOnly;
+    }
+
+    public int getType() {
+        return mTvInputInfo.getType();
+    }
+
+    @VisibleForTesting
+    public Map<String, String> getExtras() {
+        ServiceInfo si = mTvInputInfo.getServiceInfo();
+
+        try {
+            XmlPullParser parser = getXmlResourceParser();
+            int type;
+            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                    && type != XmlPullParser.START_TAG) {
+            }
+
+            if (!TV_INPUT_XML_START_TAG_NAME.equals(parser.getName())) {
+                Log.w(TAG, "Meta-data does not start with " + TV_INPUT_XML_START_TAG_NAME
+                        + " tag for " + si.name);
+                return Collections.emptyMap();
+            }
+            // <tv-input> start tag found
+            Map<String, String> extras = new HashMap<>();
+            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
+                if (type == XmlPullParser.END_TAG
+                        && TV_INPUT_XML_START_TAG_NAME.equals(parser.getName())) {
+                    // </tv-input> end tag found
+                    return extras;
+                }
+                if (type == XmlPullParser.START_TAG
+                        && TV_INPUT_EXTRA_XML_START_TAG_NAME.equals(parser.getName())) {
+                    String extraName =
+                            parser.getAttributeValue(ATTRIBUTE_NAMESPACE_ANDROID, ATTRIBUTE_NAME);
+                    String extraValue =
+                            parser.getAttributeValue(ATTRIBUTE_NAMESPACE_ANDROID, ATTRIBUTE_VALUE);
+                    if (extraName != null && extraValue != null) {
+                        extras.put(extraName, extraValue);
+                    }
+                }
+            }
+
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to get extras of " + mTvInputInfo.getId() , e);
+        }
+        return Collections.emptyMap();
+    }
+
+    @VisibleForTesting
+    XmlPullParser getXmlResourceParser() {
+        ServiceInfo si = mTvInputInfo.getServiceInfo();
+        PackageManager pm = mContext.getPackageManager();
+        return si.loadXmlMetaData(pm, TvInputService.SERVICE_META_DATA);
+    }
+}
diff --git a/common/src/com/android/tv/common/compat/TvRecordingClientCompat.java b/common/src/com/android/tv/common/compat/TvRecordingClientCompat.java
new file mode 100644
index 0000000..143ff25
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/TvRecordingClientCompat.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.common.compat;
+
+import android.content.Context;
+import android.media.tv.TvRecordingClient;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.RequiresApi;
+import android.util.ArrayMap;
+import com.android.tv.common.compat.api.PrivateCommandSender;
+import com.android.tv.common.compat.api.RecordingClientCallbackCompatEvents;
+import com.android.tv.common.compat.api.TvRecordingClientCompatCommands;
+import com.android.tv.common.compat.internal.RecordingClientCompatProcessor;
+
+/**
+ * TIF Compatibility for {@link TvRecordingClient}.
+ *
+ * <p>Extends {@code TvRecordingClient} in a backwards compatible way.
+ */
+@RequiresApi(api = VERSION_CODES.N)
+public class TvRecordingClientCompat extends TvRecordingClient
+        implements TvRecordingClientCompatCommands, PrivateCommandSender {
+
+    private final RecordingClientCompatProcessor mProcessor;
+
+    /**
+     * Creates a new TvRecordingClient object.
+     *
+     * @param context The application context to create a TvRecordingClient with.
+     * @param tag A short name for debugging purposes.
+     * @param callback The callback to receive recording status changes.
+     * @param handler The handler to invoke the callback on.
+     */
+    public TvRecordingClientCompat(
+            Context context, String tag, RecordingCallback callback, Handler handler) {
+        super(context, tag, callback, handler);
+        RecordingCallbackCompat compatEvents =
+                callback instanceof RecordingCallbackCompat
+                        ? (RecordingCallbackCompat) callback
+                        : null;
+        mProcessor = new RecordingClientCompatProcessor(this, compatEvents);
+        if (compatEvents != null) {
+            compatEvents.mClientCompatProcessor = mProcessor;
+        }
+    }
+
+    /** Tell the session to Display a debug message dev builds only. */
+    @Override
+    public void devMessage(String message) {
+        mProcessor.devMessage(message);
+    }
+
+    /**
+     * TIF Compatibility for {@link RecordingCallback}.
+     *
+     * <p>Extends {@code RecordingCallback} in a backwards compatible way.
+     */
+    public static class RecordingCallbackCompat extends RecordingCallback
+            implements RecordingClientCallbackCompatEvents {
+        private final ArrayMap<String, Integer> inputCompatVersionMap = new ArrayMap<>();
+        private RecordingClientCompatProcessor mClientCompatProcessor;
+
+        @Override
+        public void onEvent(String inputId, String eventType, Bundle eventArgs) {
+            if (mClientCompatProcessor != null
+                    && !mClientCompatProcessor.handleEvent(inputId, eventType, eventArgs)) {
+                super.onEvent(inputId, eventType, eventArgs);
+            }
+        }
+
+        public int getTifCompatVersionForInput(String inputId) {
+            return inputCompatVersionMap.containsKey(inputId)
+                    ? inputCompatVersionMap.get(inputId)
+                    : 0;
+        }
+
+        /** Display a message as a toast on dev builds only. */
+        @Override
+        public void onDevToast(String inputId, String message) {}
+
+        /** Recording started. */
+        @Override
+        public void onRecordingStarted(String inputId, String recUri) {}
+    }
+}
diff --git a/common/src/com/android/tv/common/compat/TvViewCompat.java b/common/src/com/android/tv/common/compat/TvViewCompat.java
new file mode 100644
index 0000000..f44564d
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/TvViewCompat.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.common.compat;
+
+import android.content.Context;
+import android.media.tv.TvView;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.support.annotation.RequiresApi;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import com.android.tv.common.compat.api.PrivateCommandSender;
+import com.android.tv.common.compat.api.TvInputCallbackCompatEvents;
+import com.android.tv.common.compat.api.TvViewCompatCommands;
+import com.android.tv.common.compat.internal.TvViewCompatProcessor;
+
+/**
+ * TIF Compatibility for {@link TvView}.
+ *
+ * <p>Extends {@code TvView} in a backwards compatible way.
+ */
+@RequiresApi(api = VERSION_CODES.LOLLIPOP)
+public class TvViewCompat extends TvView implements TvViewCompatCommands, PrivateCommandSender {
+
+    private final TvViewCompatProcessor mTvViewCompatProcessor;
+
+    public TvViewCompat(Context context) {
+        this(context, null);
+    }
+
+    public TvViewCompat(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public TvViewCompat(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mTvViewCompatProcessor = new TvViewCompatProcessor(this);
+    }
+
+    @Override
+    public void setCallback(TvInputCallback callback) {
+        super.setCallback(callback);
+        if (callback instanceof TvInputCallbackCompat) {
+            TvInputCallbackCompat compatEvents = (TvInputCallbackCompat) callback;
+            mTvViewCompatProcessor.setCallback(compatEvents);
+            compatEvents.mTvViewCompatProcessor = mTvViewCompatProcessor;
+        }
+    }
+
+    @Override
+    public void devMessage(String message) {
+        mTvViewCompatProcessor.devMessage(message);
+    }
+
+    /**
+     * TIF Compatibility for {@link TvInputCallback}.
+     *
+     * <p>Extends {@code TvInputCallback} in a backwards compatible way.
+     */
+    public static class TvInputCallbackCompat extends TvInputCallback
+            implements TvInputCallbackCompatEvents {
+        private final ArrayMap<String, Integer> inputCompatVersionMap = new ArrayMap<>();
+        private TvViewCompatProcessor mTvViewCompatProcessor;
+
+        @Override
+        public void onEvent(String inputId, String eventType, Bundle eventArgs) {
+            if (mTvViewCompatProcessor != null
+                    && !mTvViewCompatProcessor.handleEvent(inputId, eventType, eventArgs)) {
+                super.onEvent(inputId, eventType, eventArgs);
+            }
+        }
+
+        public int getTifCompatVersionForInput(String inputId) {
+            return inputCompatVersionMap.containsKey(inputId)
+                    ? inputCompatVersionMap.get(inputId)
+                    : 0;
+        }
+
+        @Override
+        public void onDevToast(String inputId, String message) {}
+
+        /**
+         * This is called when the signal strength is notified.
+         *
+         * @param inputId The ID of the TV input bound to this view.
+         * @param value The current signal strength. Should be one of the followings.
+         *     <ul>
+         *       <li>{@link TvInputConstantCompat#SIGNAL_STRENGTH_NOT_USED}
+         *       <li>{@link TvInputConstantCompat#SIGNAL_STRENGTH_ERROR}
+         *       <li>{@link TvInputConstantCompat#SIGNAL_STRENGTH_UNKNOWN}
+         *       <li>{int [0, 100]}
+         *     </ul>
+         */
+        @Override
+        public void onSignalStrength(String inputId, int value) {}
+    }
+}
diff --git a/src/com/android/tv/util/Filter.java b/common/src/com/android/tv/common/compat/api/PrivateCommandSender.java
similarity index 60%
copy from src/com/android/tv/util/Filter.java
copy to common/src/com/android/tv/common/compat/api/PrivateCommandSender.java
index 3e24a49..11de970 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/common/src/com/android/tv/common/compat/api/PrivateCommandSender.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -11,13 +11,14 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
- * limitations under the License.
+ * limitations under the License
  */
+package com.android.tv.common.compat.api;
 
-package com.android.tv.util;
+import android.os.Bundle;
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
+/** Sends a command from the TV App to a {@link android.media.tv.TvInputService.Session} */
+public interface PrivateCommandSender {
+
+    void sendAppPrivateCommand(String action, Bundle data);
 }
diff --git a/common/src/com/android/tv/common/compat/api/RecordingClientCallbackCompatEvents.java b/common/src/com/android/tv/common/compat/api/RecordingClientCallbackCompatEvents.java
new file mode 100644
index 0000000..753703c
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/api/RecordingClientCallbackCompatEvents.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.api;
+
+/**
+ * {@link android.media.tv.TvRecordingClient} implements this to receive notification from a {@link
+ * android.media.tv.TvInputService.RecordingSession}
+ */
+public interface RecordingClientCallbackCompatEvents {
+    /** Display a message in the application as a toast on dev builds only */
+    void onDevToast(String inputId, String message);
+
+    void onRecordingStarted(String inputId, String recUri);
+}
diff --git a/src/com/android/tv/util/Filter.java b/common/src/com/android/tv/common/compat/api/RecordingSessionCompatCommands.java
similarity index 62%
copy from src/com/android/tv/util/Filter.java
copy to common/src/com/android/tv/common/compat/api/RecordingSessionCompatCommands.java
index 3e24a49..9deaa41 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/common/src/com/android/tv/common/compat/api/RecordingSessionCompatCommands.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -11,13 +11,12 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
- * limitations under the License.
+ * limitations under the License
  */
+package com.android.tv.common.compat.api;
 
-package com.android.tv.util;
+/** Commands sent from the TV App to {@link android.media.tv.TvInputService.RecordingSession} */
+public interface RecordingSessionCompatCommands {
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
+    void onDevMessage(String message);
 }
diff --git a/common/src/com/android/tv/common/compat/api/RecordingSessionCompatEvents.java b/common/src/com/android/tv/common/compat/api/RecordingSessionCompatEvents.java
new file mode 100644
index 0000000..812bba6
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/api/RecordingSessionCompatEvents.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.api;
+
+/** Events sent from the {@link android.media.tv.TvInputService.RecordingSession} to the TV App. */
+public interface RecordingSessionCompatEvents {
+
+    void notifyDevToast(String message);
+
+    void notifyRecordingStarted(String value);
+}
diff --git a/src/com/android/tv/util/Filter.java b/common/src/com/android/tv/common/compat/api/SessionCompatCommands.java
similarity index 63%
copy from src/com/android/tv/util/Filter.java
copy to common/src/com/android/tv/common/compat/api/SessionCompatCommands.java
index 3e24a49..bef4ad2 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/common/src/com/android/tv/common/compat/api/SessionCompatCommands.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -11,13 +11,12 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
- * limitations under the License.
+ * limitations under the License
  */
+package com.android.tv.common.compat.api;
 
-package com.android.tv.util;
+/** Commands sent from the TV App to {@link android.media.tv.TvInputService.Session} */
+public interface SessionCompatCommands {
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
+    void onDevMessage(String message);
 }
diff --git a/src/com/android/tv/util/Filter.java b/common/src/com/android/tv/common/compat/api/SessionCompatEvents.java
similarity index 60%
copy from src/com/android/tv/util/Filter.java
copy to common/src/com/android/tv/common/compat/api/SessionCompatEvents.java
index 3e24a49..a3af8f3 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/common/src/com/android/tv/common/compat/api/SessionCompatEvents.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -11,13 +11,14 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
- * limitations under the License.
+ * limitations under the License
  */
+package com.android.tv.common.compat.api;
 
-package com.android.tv.util;
+/** Events sent from the {@link android.media.tv.TvInputService.Session} to the TV App. */
+public interface SessionCompatEvents {
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
+    void notifyDevToast(String message);
+
+    void notifySignalStrength(int value);
 }
diff --git a/src/com/android/tv/util/Filter.java b/common/src/com/android/tv/common/compat/api/SessionEventNotifier.java
similarity index 60%
copy from src/com/android/tv/util/Filter.java
copy to common/src/com/android/tv/common/compat/api/SessionEventNotifier.java
index 3e24a49..66c5c3a 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/common/src/com/android/tv/common/compat/api/SessionEventNotifier.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -11,13 +11,14 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
- * limitations under the License.
+ * limitations under the License
  */
+package com.android.tv.common.compat.api;
 
-package com.android.tv.util;
+import android.os.Bundle;
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
+/** Sends events from a {@link android.media.tv.TvInputService.Session} to the TV App. */
+public interface SessionEventNotifier {
+
+    void notifySessionEvent(String event, Bundle data);
 }
diff --git a/common/src/com/android/tv/common/compat/api/TvInputCallbackCompatEvents.java b/common/src/com/android/tv/common/compat/api/TvInputCallbackCompatEvents.java
new file mode 100644
index 0000000..e6b241b
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/api/TvInputCallbackCompatEvents.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.api;
+
+/**
+ * {@link android.media.tv.TvView.TvInputCallback} implements this to receive notification from a
+ * {@link android.media.tv.TvInputService.Session}
+ */
+public interface TvInputCallbackCompatEvents {
+    void onDevToast(String inputId, String message);
+
+    void onSignalStrength(String inputId, int value);
+}
diff --git a/src/com/android/tv/util/Filter.java b/common/src/com/android/tv/common/compat/api/TvRecordingClientCompatCommands.java
similarity index 62%
copy from src/com/android/tv/util/Filter.java
copy to common/src/com/android/tv/common/compat/api/TvRecordingClientCompatCommands.java
index 3e24a49..c185216 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/common/src/com/android/tv/common/compat/api/TvRecordingClientCompatCommands.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -11,13 +11,12 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
- * limitations under the License.
+ * limitations under the License
  */
+package com.android.tv.common.compat.api;
 
-package com.android.tv.util;
+/** Commands sent from the TV App to {@link android.media.tv.TvInputService.RecordingSession} */
+public interface TvRecordingClientCompatCommands {
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
+    void devMessage(String message);
 }
diff --git a/src/com/android/tv/util/Filter.java b/common/src/com/android/tv/common/compat/api/TvViewCompatCommands.java
similarity index 63%
copy from src/com/android/tv/util/Filter.java
copy to common/src/com/android/tv/common/compat/api/TvViewCompatCommands.java
index 3e24a49..5abc6bc 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/common/src/com/android/tv/common/compat/api/TvViewCompatCommands.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -11,13 +11,12 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
- * limitations under the License.
+ * limitations under the License
  */
+package com.android.tv.common.compat.api;
 
-package com.android.tv.util;
+/** Commands sent from the TV App to {@link android.media.tv.TvInputService.Session} */
+public interface TvViewCompatCommands {
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
+    void devMessage(String message);
 }
diff --git a/common/src/com/android/tv/common/compat/internal/Constants.java b/common/src/com/android/tv/common/compat/internal/Constants.java
new file mode 100644
index 0000000..993822c
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/internal/Constants.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.internal;
+/** Static constants use by the TIF compat library */
+final class Constants {
+    static final String ACTION_GET_VERSION = "com.android.tv.common.compat.action.GET_VERSION";
+    static final String EVENT_GET_VERSION = "com.android.tv.common.compat.event.GET_VERSION";
+    static final String ACTION_COMPAT_ON = "com.android.tv.common.compat.action.COMPAT_ON";
+    static final String EVENT_COMPAT_NOTIFY = "com.android.tv.common.compat.event.COMPAT_NOTIFY";
+    static final String EVENT_COMPAT_NOTIFY_ERROR =
+            "com.android.tv.common.compat.event.COMPAT_NOTIFY_ERROR";
+    static final int TIF_COMPAT_VERSION = 1;
+
+    private Constants() {}
+}
diff --git a/common/src/com/android/tv/common/compat/internal/RecordingClientCompatProcessor.java b/common/src/com/android/tv/common/compat/internal/RecordingClientCompatProcessor.java
new file mode 100644
index 0000000..f83228c
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/internal/RecordingClientCompatProcessor.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.internal;
+
+import android.support.annotation.Nullable;
+import android.util.Log;
+import com.android.tv.common.compat.api.PrivateCommandSender;
+import com.android.tv.common.compat.api.RecordingClientCallbackCompatEvents;
+import com.android.tv.common.compat.api.TvViewCompatCommands;
+import com.android.tv.common.compat.internal.Commands.OnDevMessage;
+import com.android.tv.common.compat.internal.Commands.PrivateCommand;
+import com.android.tv.common.compat.internal.RecordingEvents.NotifyDevToast;
+import com.android.tv.common.compat.internal.RecordingEvents.RecordingSessionEvent;
+
+/**
+ * Sends {@link RecordingCommands} to the {@link android.media.tv.TvInputService.RecordingSession}
+ * via {@link PrivateCommandSender} and receives notification events from the session forwarding
+ * them to {@link RecordingClientCallbackCompatEvents}
+ */
+public final class RecordingClientCompatProcessor
+        extends ViewCompatProcessor<PrivateCommand, RecordingSessionEvent>
+        implements TvViewCompatCommands {
+    private static final String TAG = "RecordingClientCompatProcessor";
+
+    @Nullable private final RecordingClientCallbackCompatEvents mCallback;
+
+    public RecordingClientCompatProcessor(
+            PrivateCommandSender commandSender,
+            @Nullable RecordingClientCallbackCompatEvents callback) {
+        super(commandSender, RecordingSessionEvent.parser());
+        mCallback = callback;
+    }
+
+    @Override
+    public void devMessage(String message) {
+        OnDevMessage devMessage = OnDevMessage.newBuilder().setMessage(message).build();
+        PrivateCommand privateCommand =
+                createPrivateCommandCommand().setOnDevMessage(devMessage).build();
+        sendCompatCommand(privateCommand);
+    }
+
+    private PrivateCommand.Builder createPrivateCommandCommand() {
+        return PrivateCommand.newBuilder().setCompatVersion(Constants.TIF_COMPAT_VERSION);
+    }
+
+    @Override
+    protected final void handleSessionEvent(String inputId, RecordingSessionEvent sessionEvent) {
+        switch (sessionEvent.getEventCase()) {
+            case NOTIFY_DEV_MESSAGE:
+                handle(inputId, sessionEvent.getNotifyDevMessage());
+                break;
+            case RECORDING_STARTED:
+                handle(inputId, sessionEvent.getRecordingStarted());
+                break;
+
+            case EVENT_NOT_SET:
+                Log.w(TAG, "Error event not set compat notify  ");
+        }
+    }
+
+    private void handle(String inputId, NotifyDevToast devToast) {
+        if (devToast != null && mCallback != null) {
+            mCallback.onDevToast(inputId, devToast.getMessage());
+        }
+    }
+
+    private void handle(String inputId, RecordingEvents.RecordingStarted recStart) {
+        if (recStart != null && mCallback != null) {
+            mCallback.onRecordingStarted(inputId, recStart.getUri());
+        }
+    }
+}
diff --git a/common/src/com/android/tv/common/compat/internal/RecordingSessionCompatProcessor.java b/common/src/com/android/tv/common/compat/internal/RecordingSessionCompatProcessor.java
new file mode 100644
index 0000000..84ec550
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/internal/RecordingSessionCompatProcessor.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.internal;
+
+import android.util.Log;
+import com.android.tv.common.compat.api.RecordingSessionCompatCommands;
+import com.android.tv.common.compat.api.RecordingSessionCompatEvents;
+import com.android.tv.common.compat.api.SessionEventNotifier;
+import com.android.tv.common.compat.internal.RecordingCommands.PrivateRecordingCommand;
+import com.android.tv.common.compat.internal.RecordingEvents.NotifyDevToast;
+import com.android.tv.common.compat.internal.RecordingEvents.RecordingSessionEvent;
+import com.android.tv.common.compat.internal.RecordingEvents.RecordingStarted;
+
+/**
+ * Sends {@link RecordingSessionCompatEvents} to the TV App via {@link SessionEventNotifier} and
+ * receives Commands from TV App forwarding them to {@link RecordingSessionCompatProcessor}
+ */
+public final class RecordingSessionCompatProcessor
+        extends SessionCompatProcessor<PrivateRecordingCommand, RecordingSessionEvent>
+        implements RecordingSessionCompatEvents {
+
+    private static final String TAG = "RecordingSessionCompatProc";
+
+    private final RecordingSessionCompatCommands mRecordingSessionOnCompat;
+
+    public RecordingSessionCompatProcessor(
+            SessionEventNotifier sessionEventNotifier,
+            RecordingSessionCompatCommands recordingSessionOnCompat) {
+        super(sessionEventNotifier, PrivateRecordingCommand.parser());
+        mRecordingSessionOnCompat = recordingSessionOnCompat;
+    }
+
+    @Override
+    protected void onCompat(PrivateRecordingCommand privateCommand) {
+        switch (privateCommand.getCommandCase()) {
+            case ON_DEV_MESSAGE:
+                mRecordingSessionOnCompat.onDevMessage(
+                        privateCommand.getOnDevMessage().getMessage());
+                break;
+            case COMMAND_NOT_SET:
+                Log.w(TAG, "Command not set ");
+        }
+    }
+
+    @Override
+    public void notifyDevToast(String message) {
+        NotifyDevToast devMessage = NotifyDevToast.newBuilder().setMessage(message).build();
+        RecordingSessionEvent sessionEvent =
+                createSessionEvent().setNotifyDevMessage(devMessage).build();
+        notifyCompat(sessionEvent);
+    }
+
+    @Override
+    public void notifyRecordingStarted(String uri) {
+        RecordingStarted event = RecordingStarted.newBuilder().setUri(uri).build();
+        RecordingSessionEvent sessionEvent =
+                createSessionEvent().setRecordingStarted(event).build();
+        notifyCompat(sessionEvent);
+    }
+
+    private RecordingSessionEvent.Builder createSessionEvent() {
+
+        return RecordingSessionEvent.newBuilder().setCompatVersion(Constants.TIF_COMPAT_VERSION);
+    }
+}
diff --git a/common/src/com/android/tv/common/compat/internal/SessionCompatProcessor.java b/common/src/com/android/tv/common/compat/internal/SessionCompatProcessor.java
new file mode 100644
index 0000000..7f27a24
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/internal/SessionCompatProcessor.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.internal;
+
+import android.os.Bundle;
+import android.util.Log;
+import com.android.tv.common.compat.api.SessionEventNotifier;
+import com.google.protobuf.GeneratedMessageLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.Parser;
+
+/**
+ * Sends {@code events} to the TV App via {@link SessionEventNotifier} and receives {@code commands}
+ * from TV App.
+ */
+abstract class SessionCompatProcessor<
+        C extends GeneratedMessageLite<C, ?>, E extends GeneratedMessageLite<E, ?>> {
+    private static final String TAG = "SessionCompatProcessor";
+    private final SessionEventNotifier mSessionEventNotifier;
+    private final Parser<C> mCommandParser;
+
+    SessionCompatProcessor(SessionEventNotifier sessionEventNotifier, Parser<C> commandParser) {
+        mSessionEventNotifier = sessionEventNotifier;
+        mCommandParser = commandParser;
+    }
+
+    public final boolean handleAppPrivateCommand(String action, Bundle data) {
+        switch (action) {
+            case Constants.ACTION_GET_VERSION:
+                Bundle response = new Bundle();
+                response.putInt(Constants.EVENT_GET_VERSION, Constants.TIF_COMPAT_VERSION);
+                mSessionEventNotifier.notifySessionEvent(Constants.EVENT_GET_VERSION, response);
+                return true;
+            case Constants.ACTION_COMPAT_ON:
+                byte[] bytes = data.getByteArray(Constants.ACTION_COMPAT_ON);
+                try {
+                    C privateCommand = mCommandParser.parseFrom(bytes);
+                    onCompat(privateCommand);
+                } catch (InvalidProtocolBufferException e) {
+                    Log.w(TAG, "Error parsing compat data", e);
+                }
+
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    abstract void onCompat(C privateCommand);
+
+    final void notifyCompat(E event) {
+        Bundle response = new Bundle();
+        try {
+            byte[] bytes = event.toByteArray();
+            response.putByteArray(Constants.EVENT_COMPAT_NOTIFY, bytes);
+        } catch (Exception e) {
+            Log.w(TAG, "Failed to send " + event, e);
+            response.putString(Constants.EVENT_COMPAT_NOTIFY_ERROR, e.getMessage());
+        }
+        mSessionEventNotifier.notifySessionEvent(Constants.EVENT_COMPAT_NOTIFY, response);
+    }
+}
diff --git a/common/src/com/android/tv/common/compat/internal/TifSessionCompatProcessor.java b/common/src/com/android/tv/common/compat/internal/TifSessionCompatProcessor.java
new file mode 100644
index 0000000..dd7a3b3
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/internal/TifSessionCompatProcessor.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.internal;
+
+import android.util.Log;
+import com.android.tv.common.compat.api.SessionCompatCommands;
+import com.android.tv.common.compat.api.SessionCompatEvents;
+import com.android.tv.common.compat.api.SessionEventNotifier;
+import com.android.tv.common.compat.internal.Commands.PrivateCommand;
+import com.android.tv.common.compat.internal.Events.NotifyDevToast;
+import com.android.tv.common.compat.internal.Events.NotifySignalStrength;
+import com.android.tv.common.compat.internal.Events.SessionEvent;
+
+/**
+ * Sends {@link SessionCompatEvents} to the TV App via {@link SessionEventNotifier} and receives
+ * Commands from TV App forwarding them to {@link SessionCompatCommands}
+ */
+public final class TifSessionCompatProcessor
+        extends SessionCompatProcessor<PrivateCommand, SessionEvent>
+        implements SessionCompatEvents {
+
+    private static final String TAG = "TifSessionCompatProcessor";
+
+    private final SessionCompatCommands mSessionOnCompat;
+
+    public TifSessionCompatProcessor(
+            SessionEventNotifier sessionEventNotifier, SessionCompatCommands sessionOnCompat) {
+        super(sessionEventNotifier, PrivateCommand.parser());
+        mSessionOnCompat = sessionOnCompat;
+    }
+
+    @Override
+    protected void onCompat(Commands.PrivateCommand privateCommand) {
+        switch (privateCommand.getCommandCase()) {
+            case ON_DEV_MESSAGE:
+                mSessionOnCompat.onDevMessage(privateCommand.getOnDevMessage().getMessage());
+                break;
+            case COMMAND_NOT_SET:
+                Log.w(TAG, "Command not set ");
+        }
+    }
+
+    @Override
+    public void notifyDevToast(String message) {
+        NotifyDevToast devMessage = NotifyDevToast.newBuilder().setMessage(message).build();
+        SessionEvent sessionEvent = createSessionEvent().setNotifyDevMessage(devMessage).build();
+        notifyCompat(sessionEvent);
+    }
+
+    @Override
+    public void notifySignalStrength(int value) {
+        NotifySignalStrength signalStrength =
+                NotifySignalStrength.newBuilder().setSignalStrength(value).build();
+        SessionEvent sessionEvent =
+                createSessionEvent().setNotifySignalStrength(signalStrength).build();
+        notifyCompat(sessionEvent);
+    }
+
+    private SessionEvent.Builder createSessionEvent() {
+        return SessionEvent.newBuilder().setCompatVersion(Constants.TIF_COMPAT_VERSION);
+    }
+}
diff --git a/common/src/com/android/tv/common/compat/internal/TvViewCompatProcessor.java b/common/src/com/android/tv/common/compat/internal/TvViewCompatProcessor.java
new file mode 100644
index 0000000..382f8d8
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/internal/TvViewCompatProcessor.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.internal;
+
+import android.support.annotation.NonNull;
+import android.util.Log;
+import com.android.tv.common.compat.api.PrivateCommandSender;
+import com.android.tv.common.compat.api.TvInputCallbackCompatEvents;
+import com.android.tv.common.compat.api.TvViewCompatCommands;
+import com.android.tv.common.compat.internal.Commands.OnDevMessage;
+import com.android.tv.common.compat.internal.Commands.PrivateCommand;
+import com.android.tv.common.compat.internal.Events.NotifyDevToast;
+import com.android.tv.common.compat.internal.Events.NotifySignalStrength;
+import com.android.tv.common.compat.internal.Events.SessionEvent;
+
+/**
+ * Sends {@link TvViewCompatCommands} to the {@link android.media.tv.TvInputService.Session} via
+ * {@link PrivateCommandSender} and receives notification events from the session forwarding them to
+ * {@link TvInputCallbackCompatEvents}
+ */
+public final class TvViewCompatProcessor extends ViewCompatProcessor<PrivateCommand, SessionEvent>
+        implements TvViewCompatCommands {
+    private static final String TAG = "TvViewCompatProcessor";
+
+    private TvInputCallbackCompatEvents mCallback;
+
+    public TvViewCompatProcessor(PrivateCommandSender commandSender) {
+        super(commandSender, SessionEvent.parser());
+    }
+
+    @Override
+    public void devMessage(String message) {
+        OnDevMessage devMessage = Commands.OnDevMessage.newBuilder().setMessage(message).build();
+        Commands.PrivateCommand privateCommand =
+                createPrivateCommandCommand().setOnDevMessage(devMessage).build();
+        sendCompatCommand(privateCommand);
+    }
+
+    @NonNull
+    private PrivateCommand.Builder createPrivateCommandCommand() {
+        return PrivateCommand.newBuilder().setCompatVersion(Constants.TIF_COMPAT_VERSION);
+    }
+
+    public void onDevToast(String inputId, String message) {}
+
+    public void onSignalStrength(String inputId, int value) {}
+
+    @Override
+    protected final void handleSessionEvent(String inputId, Events.SessionEvent sessionEvent) {
+        switch (sessionEvent.getEventCase()) {
+            case NOTIFY_DEV_MESSAGE:
+                handle(inputId, sessionEvent.getNotifyDevMessage());
+                break;
+            case NOTIFY_SIGNAL_STRENGTH:
+                handle(inputId, sessionEvent.getNotifySignalStrength());
+                break;
+            case EVENT_NOT_SET:
+                Log.w(TAG, "Error event not set compat notify  ");
+        }
+    }
+
+    private void handle(String inputId, NotifyDevToast devToast) {
+        if (devToast != null && mCallback != null) {
+            mCallback.onDevToast(inputId, devToast.getMessage());
+        }
+    }
+
+    private void handle(String inputId, NotifySignalStrength signalStrength) {
+        if (signalStrength != null && mCallback != null) {
+            mCallback.onSignalStrength(inputId, signalStrength.getSignalStrength());
+        }
+    }
+
+    public void setCallback(TvInputCallbackCompatEvents callback) {
+        this.mCallback = callback;
+    }
+}
diff --git a/common/src/com/android/tv/common/compat/internal/ViewCompatProcessor.java b/common/src/com/android/tv/common/compat/internal/ViewCompatProcessor.java
new file mode 100644
index 0000000..dc6bbfa
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/internal/ViewCompatProcessor.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.compat.internal;
+
+import android.os.Bundle;
+import android.util.ArrayMap;
+import android.util.Log;
+import com.android.tv.common.compat.api.PrivateCommandSender;
+import com.google.protobuf.GeneratedMessageLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.Parser;
+
+/**
+ * Sends {@code commands} to the {@code session} via {@link PrivateCommandSender} and receives
+ * notification events from the session forwarding them to {@link
+ * com.android.tv.common.compat.api.TvInputCallbackCompatEvents}
+ */
+abstract class ViewCompatProcessor<
+        C extends GeneratedMessageLite<C, ?>, E extends GeneratedMessageLite<E, ?>> {
+    private static final String TAG = "ViewCompatProcessor";
+    private final ArrayMap<String, Integer> inputCompatVersionMap = new ArrayMap<>();
+
+    private final Parser<E> mEventParser;
+    private final PrivateCommandSender mCommandSender;
+
+    ViewCompatProcessor(PrivateCommandSender commandSender, Parser<E> eventParser) {
+        mCommandSender = commandSender;
+        mEventParser = eventParser;
+    }
+
+    private final E sessionEventFromBundle(Bundle eventArgs) throws InvalidProtocolBufferException {
+
+        byte[] protoBytes = eventArgs.getByteArray(Constants.EVENT_COMPAT_NOTIFY);
+        return protoBytes == null || protoBytes.length == 0
+                ? null
+                : mEventParser.parseFrom(protoBytes);
+    }
+
+    final void sendCompatCommand(C privateCommand) {
+        try {
+            Bundle data = new Bundle();
+            data.putByteArray(Constants.ACTION_COMPAT_ON, privateCommand.toByteArray());
+            mCommandSender.sendAppPrivateCommand(Constants.ACTION_COMPAT_ON, data);
+        } catch (Exception e) {
+            Log.w(TAG, "Error sending compat action " + privateCommand, e);
+        }
+    }
+
+    public boolean handleEvent(String inputId, String eventType, Bundle eventArgs) {
+        switch (eventType) {
+            case Constants.EVENT_GET_VERSION:
+                int version = eventArgs.getInt(Constants.EVENT_GET_VERSION, 0);
+                inputCompatVersionMap.put(inputId, version);
+                return true;
+            case Constants.EVENT_COMPAT_NOTIFY:
+                try {
+                    E sessionEvent = sessionEventFromBundle(eventArgs);
+                    if (sessionEvent != null) {
+                        handleSessionEvent(inputId, sessionEvent);
+                    } else {
+                        String errorMessage =
+                                eventArgs.getString(Constants.EVENT_COMPAT_NOTIFY_ERROR);
+                        Log.w(TAG, "Error sent in compat notify  " + errorMessage);
+                    }
+
+                } catch (InvalidProtocolBufferException e) {
+                    Log.w(TAG, "Error parsing in compat notify for  " + inputId);
+                }
+
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    protected abstract void handleSessionEvent(String inputId, E sessionEvent);
+}
diff --git a/common/src/com/android/tv/common/compat/internal/recording_commands.proto b/common/src/com/android/tv/common/compat/internal/recording_commands.proto
new file mode 100644
index 0000000..ce59bfa
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/internal/recording_commands.proto
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// A set of Private Commands to send to a TVInputService.Session, in particular
+// support new features on older devices. NOTE: this proto is internal to this
+// package and should not be used outside it.
+
+syntax = "proto3";
+package android.tv.common.compat.internal;
+
+option java_outer_classname = "RecordingCommands";
+option java_package = "com.android.tv.common.compat.internal";
+
+// Wraps messages for sending to a session a private command.
+message PrivateRecordingCommand {
+  uint32 compat_version = 1;
+
+  oneof command {
+    OnDevMessage on_dev_message = 2;
+  }
+}
+
+// Display a debug message dev builds only.
+message OnDevMessage {
+  string message = 1;
+}
diff --git a/common/src/com/android/tv/common/compat/internal/recording_events.proto b/common/src/com/android/tv/common/compat/internal/recording_events.proto
new file mode 100644
index 0000000..68db5dd
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/internal/recording_events.proto
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// A set of Session events to send from a TVInputService.Session, in particular
+// support new features on older devices. NOTE: this proto is internal to this
+// package and should not be used outside it.
+syntax = "proto3";
+package android.tv.common.compat.internal;
+
+option java_outer_classname = "RecordingEvents";
+option java_package = "com.android.tv.common.compat.internal";
+
+// Wraps messages for sending from a session as an Event.
+// RecordingSessionCompat will have a notify{EventMessageName} for each event
+// TvRecordingClientCompat will have a on{EventMessageName} for each event
+
+message RecordingSessionEvent {
+  uint32 compat_version = 1;
+
+  oneof event {
+    NotifyDevToast notify_dev_message = 2;
+    RecordingStarted recording_started = 3;
+  }
+}
+
+// Display a message as a toast on dev builds only
+message NotifyDevToast {
+  string message = 1;
+}
+
+// Recording started.
+message RecordingStarted {
+  // Recording URI.
+  string uri = 1;
+}
+
diff --git a/common/src/com/android/tv/common/compat/internal/tif_commands.proto b/common/src/com/android/tv/common/compat/internal/tif_commands.proto
new file mode 100644
index 0000000..d586770
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/internal/tif_commands.proto
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// A set of Private Commands to send to a TVInputService.Session, in particular
+// support new features on older devices. NOTE: this proto is internal to this
+// package and should not be used outside it.
+
+syntax = "proto3";
+package android.tv.common.compat.internal;
+
+option java_outer_classname = "Commands";
+option java_package = "com.android.tv.common.compat.internal";
+
+// Wraps messages for sending to a session a private command.
+message PrivateCommand {
+  uint32 compat_version = 1;
+
+  oneof command {
+    OnDevMessage on_dev_message = 2;
+  }
+}
+
+// Sends a debug message to the session for display on dev builds only
+message OnDevMessage {
+  string message = 1;
+}
diff --git a/common/src/com/android/tv/common/compat/internal/tif_events.proto b/common/src/com/android/tv/common/compat/internal/tif_events.proto
new file mode 100644
index 0000000..6e71ae1
--- /dev/null
+++ b/common/src/com/android/tv/common/compat/internal/tif_events.proto
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// A set of Session events to send from a TVInputService.Session, in particular
+// support new features on older devices. NOTE: this proto is internal to this
+// package and should not be used outside it.
+syntax = "proto3";
+package android.tv.common.compat.internal;
+
+option java_outer_classname = "Events";
+option java_package = "com.android.tv.common.compat.internal";
+
+// Wraps messages for sending from a session as an Event.
+message SessionEvent {
+  uint32 compat_version = 1;
+
+  oneof event {
+    NotifyDevToast notify_dev_message = 2;
+    NotifySignalStrength notify_signal_strength = 3;
+  }
+}
+
+// Send a message to the application to be displayed as a toast on dev builds
+// only
+message NotifyDevToast {
+  string message = 1;
+}
+
+// Notifies the TV Application the current signal strength.
+message NotifySignalStrength {
+  // The signal strength as a percent (0 to 100),
+  // with -1 meaning unknown, -2 meaning not used.
+  int32 signal_strength = 1;
+}
diff --git a/common/src/com/android/tv/common/config/DefaultConfigManager.java b/common/src/com/android/tv/common/config/DefaultConfigManager.java
deleted file mode 100644
index ae24085..0000000
--- a/common/src/com/android/tv/common/config/DefaultConfigManager.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tv.common.config;
-
-import android.content.Context;
-import com.android.tv.common.config.api.RemoteConfig;
-
-/** Stub Remote Config. */
-public class DefaultConfigManager {
-    public static final long DEFAULT_LONG_VALUE = 0;
-
-    public static DefaultConfigManager createInstance(Context context) {
-        return new DefaultConfigManager();
-    }
-
-    private StubRemoteConfig mRemoteConfig = new StubRemoteConfig();
-
-    public RemoteConfig getRemoteConfig() {
-        return mRemoteConfig;
-    }
-
-    private static class StubRemoteConfig implements RemoteConfig {
-        @Override
-        public void fetch(OnRemoteConfigUpdatedListener listener) {}
-
-        @Override
-        public String getString(String key) {
-            return null;
-        }
-
-        @Override
-        public boolean getBoolean(String key) {
-            return false;
-        }
-
-        @Override
-        public long getLong(String key) {
-            return DEFAULT_LONG_VALUE;
-        }
-
-        @Override
-        public long getLong(String key, long defaultValue) {
-            return defaultValue;
-        }
-    }
-}
diff --git a/common/src/com/android/tv/common/config/RemoteConfigFeature.java b/common/src/com/android/tv/common/config/RemoteConfigFeature.java
deleted file mode 100644
index 2ea381f..0000000
--- a/common/src/com/android/tv/common/config/RemoteConfigFeature.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.tv.common.config;
-
-import android.content.Context;
-import com.android.tv.common.BaseApplication;
-import com.android.tv.common.feature.Feature;
-
-/**
- * A {@link Feature} controlled by a {@link com.android.tv.common.config.api.RemoteConfig} boolean.
- */
-public class RemoteConfigFeature implements Feature {
-    private final String mKey;
-
-    /** Creates a {@link RemoteConfigFeature for the {@code key}. */
-    public static RemoteConfigFeature fromKey(String key) {
-        return new RemoteConfigFeature(key);
-    }
-
-    private RemoteConfigFeature(String key) {
-        mKey = key;
-    }
-
-    @Override
-    public boolean isEnabled(Context context) {
-        return BaseApplication.getSingletons(context).getRemoteConfig().getBoolean(mKey);
-    }
-}
diff --git a/common/src/com/android/tv/common/config/api/RemoteConfig.java b/common/src/com/android/tv/common/config/api/RemoteConfig.java
deleted file mode 100644
index 74597f9..0000000
--- a/common/src/com/android/tv/common/config/api/RemoteConfig.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tv.common.config.api;
-
-/**
- * Manages Live TV Configuration, allowing remote updates.
- *
- * <p>This is a thin wrapper around <a
- * href="https://firebase.google.com/docs/remote-config/"></a>Firebase Remote Config</a>
- */
-public interface RemoteConfig {
-
-    /** Used to inject a remote config */
-    interface HasRemoteConfig {
-        RemoteConfig getRemoteConfig();
-    }
-
-    /** Notified on successful completion of a {@link #fetch)} */
-    interface OnRemoteConfigUpdatedListener {
-        void onRemoteConfigUpdated();
-    }
-
-    /** Starts a fetch and notifies {@code listener} on successful completion. */
-    void fetch(OnRemoteConfigUpdatedListener listener);
-
-    /** Gets value as a string corresponding to the specified key. */
-    String getString(String key);
-
-    /** Gets value as a boolean corresponding to the specified key. */
-    boolean getBoolean(String key);
-
-    /** Gets value as a long corresponding to the specified key. */
-    long getLong(String key);
-
-    /**
-     * Gets value as a long corresponding to the specified key. Returns the defaultValue if no value
-     * is found.
-     */
-    long getLong(String key, long defaultValue);
-}
diff --git a/common/src/com/android/tv/common/config/api/RemoteConfigValue.java b/common/src/com/android/tv/common/config/api/RemoteConfigValue.java
deleted file mode 100644
index 6da89fb..0000000
--- a/common/src/com/android/tv/common/config/api/RemoteConfigValue.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.tv.common.config.api;
-
-/** Wrapper for a RemoteConfig key and default value. */
-public abstract class RemoteConfigValue<T> {
-    private final T defaultValue;
-    private final String key;
-
-    private RemoteConfigValue(String key, T defaultValue) {
-        this.defaultValue = defaultValue;
-        this.key = key;
-    }
-
-    /** Create with the given key and default value */
-    public static RemoteConfigValue<Long> create(String key, long defaultValue) {
-        return new RemoteConfigValue<Long>(key, defaultValue) {
-            @Override
-            public Long get(RemoteConfig remoteConfig) {
-                return remoteConfig.getLong(key, defaultValue);
-            }
-        };
-    }
-
-    public abstract T get(RemoteConfig remoteConfig);
-
-    public final T getDefaultValue() {
-        return defaultValue;
-    }
-
-    @Override
-    public final String toString() {
-        return "RemoteConfigValue(key=" + key + ", defalutValue=" + defaultValue + "]";
-    }
-}
diff --git a/common/src/com/android/tv/common/dagger/ApplicationModule.java b/common/src/com/android/tv/common/dagger/ApplicationModule.java
new file mode 100644
index 0000000..4655f77
--- /dev/null
+++ b/common/src/com/android/tv/common/dagger/ApplicationModule.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.dagger;
+
+import android.app.Application;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.Looper;
+import com.android.tv.common.dagger.annotations.ApplicationContext;
+import com.android.tv.common.dagger.annotations.MainLooper;
+import dagger.Module;
+import dagger.Provides;
+
+/**
+ * Provides application-scope qualifiers for the {@link Application}, the application context, and
+ * the application's main looper.
+ */
+@Module
+public final class ApplicationModule {
+    private final Application mApplication;
+
+    public ApplicationModule(Application application) {
+        mApplication = application;
+    }
+
+    @Provides
+    Application provideApplication() {
+        return mApplication;
+    }
+
+    @Provides
+    @ApplicationContext
+    Context provideContext() {
+        return mApplication.getApplicationContext();
+    }
+
+    @Provides
+    @MainLooper
+    static Looper provideMainLooper() {
+        return Looper.getMainLooper();
+    }
+
+    @Provides
+    ContentResolver provideContentResolver() {
+        return mApplication.getContentResolver();
+    }
+}
diff --git a/src/com/android/tv/util/Filter.java b/common/src/com/android/tv/common/dagger/annotations/ApplicationContext.java
similarity index 67%
copy from src/com/android/tv/util/Filter.java
copy to common/src/com/android/tv/common/dagger/annotations/ApplicationContext.java
index 3e24a49..8631815 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/common/src/com/android/tv/common/dagger/annotations/ApplicationContext.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2019 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,11 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package com.android.tv.common.dagger.annotations;
 
-package com.android.tv.util;
+import javax.inject.Qualifier;
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
-}
+/** Annotation for requesting the application's context. */
+@Qualifier
+public @interface ApplicationContext {}
diff --git a/src/com/android/tv/util/Filter.java b/common/src/com/android/tv/common/dagger/annotations/MainLooper.java
similarity index 67%
copy from src/com/android/tv/util/Filter.java
copy to common/src/com/android/tv/common/dagger/annotations/MainLooper.java
index 3e24a49..a8b4100 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/common/src/com/android/tv/common/dagger/annotations/MainLooper.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2019 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,11 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package com.android.tv.common.dagger.annotations;
 
-package com.android.tv.util;
+import javax.inject.Qualifier;
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
-}
+/** Annotation for requesting a Looper that is on the UI thread. */
+@Qualifier
+public @interface MainLooper {}
diff --git a/common/src/com/android/tv/common/data/RecordedProgramState.java b/common/src/com/android/tv/common/data/RecordedProgramState.java
new file mode 100644
index 0000000..3bfad88
--- /dev/null
+++ b/common/src/com/android/tv/common/data/RecordedProgramState.java
@@ -0,0 +1,14 @@
+package com.android.tv.common.data;
+
+/** The recording state. */
+// TODO(b/25023911): Use @SimpleEnum when it is supported by AutoValue
+public enum RecordedProgramState {
+    // TODO(b/71717809): Document each state.
+    NOT_SET,
+    STARTED,
+    FINISHED,
+    PARTIAL,
+    FAILED,
+    DELETE,
+    DELETED,
+}
diff --git a/common/src/com/android/tv/common/experiments/ExperimentFlag.java b/common/src/com/android/tv/common/experiments/ExperimentFlag.java
index c9bacac..b8370ad 100644
--- a/common/src/com/android/tv/common/experiments/ExperimentFlag.java
+++ b/common/src/com/android/tv/common/experiments/ExperimentFlag.java
@@ -19,41 +19,63 @@
 import android.support.annotation.VisibleForTesting;
 import com.android.tv.common.BuildConfig;
 
+import com.google.common.base.Supplier;
 
 /** Experiments return values based on user, device and other criteria. */
 public final class ExperimentFlag<T> {
 
+    // NOTE: sAllowOverrides IS NEVER USED in the non AOSP version.
     private static boolean sAllowOverrides = false;
 
     @VisibleForTesting
     public static void initForTest() {
+        /* Begin_AOSP_Comment_Out
+        if (!BuildConfig.AOSP) {
+            PhenotypeFlag.initForTest();
+            return;
+        }
+        End_AOSP_Comment_Out */
         sAllowOverrides = true;
     }
 
     /** Returns a boolean experiment */
     public static ExperimentFlag<Boolean> createFlag(
+// AOSP_Comment_Out             Supplier<Boolean> phenotypeFlag,
             boolean defaultValue) {
         return new ExperimentFlag<>(
+// AOSP_Comment_Out                 phenotypeFlag,
                 defaultValue);
     }
 
     private final T mDefaultValue;
+// AOSP_Comment_Out     private final Supplier<T> mPhenotypeFlag;
 
+// AOSP_Comment_Out     // NOTE: mOverrideValue IS NEVER USED in the non AOSP version.
     private T mOverrideValue = null;
+    // mOverridden IS NEVER USED in the non AOSP version.
     private boolean mOverridden = false;
 
     private ExperimentFlag(
+// AOSP_Comment_Out             Supplier<T> phenotypeFlag,
+            // NOTE: defaultValue IS NEVER USED in the non AOSP version.
             T defaultValue) {
         mDefaultValue = defaultValue;
+// AOSP_Comment_Out         mPhenotypeFlag = phenotypeFlag;
     }
 
     /** Returns value for this experiment */
     public T get() {
+        /* Begin_AOSP_Comment_Out
+        if (!BuildConfig.AOSP) {
+            return mPhenotypeFlag.get();
+        }
+        End_AOSP_Comment_Out */
         return sAllowOverrides && mOverridden ? mOverrideValue : mDefaultValue;
     }
 
     @VisibleForTesting
     public void override(T t) {
+
         if (sAllowOverrides) {
             mOverridden = true;
             mOverrideValue = t;
@@ -64,4 +86,11 @@
     public void resetOverride() {
         mOverridden = false;
     }
+
+    /* Begin_AOSP_Comment_Out
+    @VisibleForTesting
+    T getAospDefaultValueForTesting() {
+        return mDefaultValue;
+    }
+    End_AOSP_Comment_Out */
 }
diff --git a/common/src/com/android/tv/common/experiments/Experiments.java b/common/src/com/android/tv/common/experiments/Experiments.java
index 96b15e5..9bfdb54 100644
--- a/common/src/com/android/tv/common/experiments/Experiments.java
+++ b/common/src/com/android/tv/common/experiments/Experiments.java
@@ -19,6 +19,7 @@
 import static com.android.tv.common.experiments.ExperimentFlag.createFlag;
 
 import com.android.tv.common.BuildConfig;
+// AOSP_Comment_Out import com.android.tv.common.flags.LiveChannels;
 
 /**
  * Set of experiments visible in AOSP.
@@ -26,17 +27,15 @@
  * <p>This file is maintained by hand.
  */
 public final class Experiments {
-    public static final ExperimentFlag<Boolean> CLOUD_EPG =
-            ExperimentFlag.createFlag(
-                    true);
-
     public static final ExperimentFlag<Boolean> ENABLE_UNRATED_CONTENT_SETTINGS =
             ExperimentFlag.createFlag(
+// AOSP_Comment_Out                     LiveChannels::enableUnratedContentSettings,
                     false);
 
     /** Turn analytics on or off based on the System Checkbox for logging. */
     public static final ExperimentFlag<Boolean> ENABLE_ANALYTICS_VIA_CHECKBOX =
             createFlag(
+// AOSP_Comment_Out                     LiveChannels::enableAnalyticsViaCheckbox,
                     false);
 
     /**
@@ -46,6 +45,7 @@
      */
     public static final ExperimentFlag<Boolean> ENABLE_DEVELOPER_FEATURES =
             ExperimentFlag.createFlag(
+// AOSP_Comment_Out                     LiveChannels::enableDeveloperFeatures,
                     BuildConfig.ENG);
 
     /**
@@ -57,6 +57,7 @@
      */
     public static final ExperimentFlag<Boolean> ENABLE_QA_FEATURES =
             ExperimentFlag.createFlag(
+// AOSP_Comment_Out                     LiveChannels::enableQaFeatures,
                     false);
 
     private Experiments() {}
diff --git a/common/src/com/android/tv/common/feature/EngOnlyFeature.java b/common/src/com/android/tv/common/feature/BuildTypeFeature.java
similarity index 66%
rename from common/src/com/android/tv/common/feature/EngOnlyFeature.java
rename to common/src/com/android/tv/common/feature/BuildTypeFeature.java
index 5feb548..9e1704e 100644
--- a/common/src/com/android/tv/common/feature/EngOnlyFeature.java
+++ b/common/src/com/android/tv/common/feature/BuildTypeFeature.java
@@ -20,18 +20,23 @@
 import com.android.tv.common.BuildConfig;
 
 /** A feature that is only available on {@link BuildConfig#ENG} builds. */
-public final class EngOnlyFeature implements Feature {
-    public static final Feature ENG_ONLY_FEATURE = new EngOnlyFeature();
+public final class BuildTypeFeature implements Feature {
+    public static final Feature ENG_ONLY_FEATURE = new BuildTypeFeature(BuildConfig.ENG);
+    public static final Feature ASOP_FEATURE = new BuildTypeFeature(BuildConfig.AOSP);
 
-    private EngOnlyFeature() {}
+    private final boolean mIsBuildType;
+
+    private BuildTypeFeature(boolean isBuildType) {
+        mIsBuildType = isBuildType;
+    }
 
     @Override
     public boolean isEnabled(Context context) {
-        return BuildConfig.ENG;
+        return mIsBuildType;
     }
 
     @Override
     public String toString() {
-        return "EngOnlyFeature(" + BuildConfig.ENG + ")";
+        return getClass().getSimpleName() + "(" + mIsBuildType + ")";
     }
 }
diff --git a/common/src/com/android/tv/common/feature/CommonFeatures.java b/common/src/com/android/tv/common/feature/CommonFeatures.java
index 1fceabb..04052a7 100644
--- a/common/src/com/android/tv/common/feature/CommonFeatures.java
+++ b/common/src/com/android/tv/common/feature/CommonFeatures.java
@@ -16,18 +16,16 @@
 
 package com.android.tv.common.feature;
 
-import static com.android.tv.common.feature.FeatureUtils.AND;
+import static com.android.tv.common.feature.BuildTypeFeature.ENG_ONLY_FEATURE;
+import static com.android.tv.common.feature.FeatureUtils.and;
+import static com.android.tv.common.feature.FeatureUtils.or;
 import static com.android.tv.common.feature.TestableFeature.createTestableFeature;
 
 import android.content.Context;
-import android.os.Build;
-import android.text.TextUtils;
 import android.util.Log;
-import com.android.tv.common.config.api.RemoteConfig.HasRemoteConfig;
-import com.android.tv.common.experiments.Experiments;
-
-import com.android.tv.common.util.CommonUtils;
+import com.android.tv.common.flags.has.HasCloudEpgFlags;
 import com.android.tv.common.util.LocationUtils;
+import com.android.tv.common.flags.CloudEpgFlags;
 
 /**
  * List of {@link Feature} that affect more than just the Live TV app.
@@ -46,7 +44,7 @@
      * <p>DVR API is introduced in N, it only works when app runs as a system app.
      */
     public static final TestableFeature DVR =
-            createTestableFeature(AND(Sdk.AT_LEAST_N, SystemAppFeature.SYSTEM_APP_FEATURE));
+            createTestableFeature(and(Sdk.AT_LEAST_N, SystemAppFeature.SYSTEM_APP_FEATURE));
 
     /**
      * ENABLE_RECORDING_REGARDLESS_OF_STORAGE_STATUS
@@ -56,44 +54,32 @@
     public static final Feature FORCE_RECORDING_UNTIL_NO_SPACE =
             PropertyFeature.create("force_recording_until_no_space", false);
 
-    public static final Feature TUNER =
-            new Feature() {
-                @Override
-                public boolean isEnabled(Context context) {
-
-                    if (CommonUtils.isDeveloper()) {
-                        // we enable tuner for developers to test tuner in any platform.
-                        return true;
-                    }
-
-                    // This is special handling just for USB Tuner.
-                    // It does not require any N API's but relies on a improvements in N for AC3
-                    // support
-                    return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
-                }
-            };
-
     /** Show postal code fragment before channel scan. */
     public static final Feature ENABLE_CLOUD_EPG_REGION =
-            new Feature() {
-                private final String[] supportedRegions = {
-                };
+            or(
+                    FlagFeature.from(HasCloudEpgFlags::fromContext, CloudEpgFlags::supportedRegion),
+                    new Feature() {
+                        private final String[] supportedRegions = {
+// AOSP_Comment_Out                             "US", "GB"
+                        };
 
-
-                @Override
-                public boolean isEnabled(Context context) {
-                    if (!Experiments.CLOUD_EPG.get()) {
-                        if (DEBUG) Log.d(TAG, "Experiments.CLOUD_EPG is false");
-                        return false;
-                    }
-                    String country = LocationUtils.getCurrentCountry(context);
-                    for (int i = 0; i < supportedRegions.length; i++) {
-                        if (supportedRegions[i].equalsIgnoreCase(country)) {
-                            return true;
+                        @Override
+                        public boolean isEnabled(Context context) {
+                            String country = LocationUtils.getCurrentCountry(context);
+                            for (int i = 0; i < supportedRegions.length; i++) {
+                                if (supportedRegions[i].equalsIgnoreCase(country)) {
+                                    return true;
+                                }
+                            }
+                            if (DEBUG) Log.d(TAG, "EPG flag false after country check");
+                            return false;
                         }
-                    }
-                    if (DEBUG) Log.d(TAG, "EPG flag false after country check");
-                    return false;
-                }
-            };
+                    });
+
+    // TODO(b/74197177): remove when UI and API finalized.
+    /** Show channel signal strength. */
+    public static final Feature TUNER_SIGNAL_STRENGTH = ENG_ONLY_FEATURE;
+
+    /** Use AudioOnlyTvService for audio-only inputs. */
+    public static final Feature ENABLE_TV_SERVICE = ENG_ONLY_FEATURE;
 }
diff --git a/common/src/com/android/tv/common/feature/FeatureUtils.java b/common/src/com/android/tv/common/feature/FeatureUtils.java
index 8650d15..aaed6c8 100644
--- a/common/src/com/android/tv/common/feature/FeatureUtils.java
+++ b/common/src/com/android/tv/common/feature/FeatureUtils.java
@@ -17,6 +17,7 @@
 package com.android.tv.common.feature;
 
 import android.content.Context;
+import com.android.tv.common.BuildConfig;
 import com.android.tv.common.util.CommonUtils;
 import java.util.Arrays;
 
@@ -28,7 +29,7 @@
      *
      * @param features the features to or
      */
-    public static Feature OR(final Feature... features) {
+    public static Feature or(final Feature... features) {
         return new Feature() {
             @Override
             public boolean isEnabled(Context context) {
@@ -52,7 +53,7 @@
      *
      * @param features the features to and
      */
-    public static Feature AND(final Feature... features) {
+    public static Feature and(final Feature... features) {
         return new Feature() {
             @Override
             public boolean isEnabled(Context context) {
@@ -70,6 +71,42 @@
             }
         };
     }
+    /**
+     * A feature available in AOSP.
+     *
+     * @param googleFeature the feature used in non AOSP builds
+     * @param aospFeature the feature used in AOSP builds
+     */
+    public static Feature aospFeature(
+// AOSP_Comment_Out             final Feature googleFeature,
+            final Feature aospFeature) {
+        /* Begin_AOSP_Comment_Out
+        if (!BuildConfig.AOSP) {
+            return googleFeature;
+        } else {
+            End_AOSP_Comment_Out */
+            return aospFeature;
+// AOSP_Comment_Out         }
+    }
+
+    /**
+     * Returns a feature that is opposite of the given {@code feature}.
+     *
+     * @param feature the feature to invert
+     */
+    public static Feature not(final Feature feature) {
+        return new Feature() {
+            @Override
+            public boolean isEnabled(Context context) {
+                return !feature.isEnabled(context);
+            }
+
+            @Override
+            public String toString() {
+                return "not(" + feature + ")";
+            }
+        };
+    }
 
     /** A feature that is always enabled. */
     public static final Feature ON =
diff --git a/common/src/com/android/tv/common/feature/FlagFeature.java b/common/src/com/android/tv/common/feature/FlagFeature.java
new file mode 100644
index 0000000..8da470e
--- /dev/null
+++ b/common/src/com/android/tv/common/feature/FlagFeature.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.feature;
+
+import android.content.Context;
+import com.google.common.base.Function;
+
+/** Feature from a Flag */
+public class FlagFeature<T> implements Feature {
+
+    private final Function<Context, T> mToFlag;
+    private final Function<T, Boolean> mToBoolean;
+
+    public static <T> FlagFeature<T> from(
+            Function<Context, T> toFlag, Function<T, Boolean> toBoolean) {
+        return new FlagFeature<T>(toFlag, toBoolean);
+    }
+
+    private FlagFeature(Function<Context, T> toFlag, Function<T, Boolean> toBoolean) {
+        mToFlag = toFlag;
+        mToBoolean = toBoolean;
+    }
+
+    @Override
+    public boolean isEnabled(Context context) {
+        return mToBoolean.apply(mToFlag.apply(context));
+    }
+
+    @Override
+    public String toString() {
+        return mToBoolean.toString();
+    }
+}
diff --git a/common/src/com/android/tv/common/feature/GServiceFeature.java b/common/src/com/android/tv/common/feature/GServiceFeature.java
deleted file mode 100644
index 1d7d115..0000000
--- a/common/src/com/android/tv/common/feature/GServiceFeature.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.tv.common.feature;
-
-import android.content.Context;
-
-/** A feature controlled by a GServices flag. */
-public class GServiceFeature implements Feature {
-    private static final String LIVECHANNELS_PREFIX = "livechannels:";
-    private final String mKey;
-    private final boolean mDefaultValue;
-
-    public GServiceFeature(String key, boolean defaultValue) {
-        mKey = LIVECHANNELS_PREFIX + key;
-        mDefaultValue = defaultValue;
-    }
-
-    @Override
-    public boolean isEnabled(Context context) {
-
-        // GServices is not available outside of Google.
-        return mDefaultValue;
-    }
-
-    @Override
-    public String toString() {
-        return "GService[hash=" + mKey.hashCode() + "]";
-    }
-}
diff --git a/common/src/com/android/tv/common/feature/Sdk.java b/common/src/com/android/tv/common/feature/Sdk.java
index 155b391..4b0a925 100644
--- a/common/src/com/android/tv/common/feature/Sdk.java
+++ b/common/src/com/android/tv/common/feature/Sdk.java
@@ -17,25 +17,33 @@
 package com.android.tv.common.feature;
 
 import android.content.Context;
-import android.os.Build;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
 
 /** Holder for SDK version features */
 public final class Sdk {
-    public static final Feature AT_LEAST_N =
-            new Feature() {
-                @Override
-                public boolean isEnabled(Context context) {
-                    return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
-                }
-            };
 
-    public static final Feature AT_LEAST_O =
-            new Feature() {
-                @Override
-                public boolean isEnabled(Context context) {
-                    return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
-                }
-            };
+    public static final Feature AT_LEAST_M = new AtLeast(VERSION_CODES.M);
+
+    public static final Feature AT_LEAST_N = new AtLeast(VERSION_CODES.N);
+
+    public static final Feature AT_LEAST_O = new AtLeast(VERSION_CODES.O);
+
+    public static final Feature AT_LEAST_P = new AtLeast(VERSION_CODES.P); // AOSP_OC:strip_line
+
+    private static final class AtLeast implements Feature {
+
+        private final int versionCode;
+
+        private AtLeast(int versionCode) {
+            this.versionCode = versionCode;
+        }
+
+        @Override
+        public boolean isEnabled(Context unused) {
+            return VERSION.SDK_INT >= versionCode;
+        }
+    }
 
     private Sdk() {}
 }
diff --git a/common/src/com/android/tv/common/flags/BackendKnobsFlags.java b/common/src/com/android/tv/common/flags/BackendKnobsFlags.java
new file mode 100644
index 0000000..69bac7a
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/BackendKnobsFlags.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.flags;
+
+/** Flags for tuning non ui behavior */
+public interface BackendKnobsFlags {
+
+    /**
+     * Whether or not this feature is compiled into this build.
+     *
+     * <p>This returns true by default, unless the is_compiled_selector parameter was set during
+     * code generation.
+     */
+    boolean compiled();
+
+    /** Enable fetching only part of the program data. */
+    boolean enablePartialProgramFetch();
+
+    /** EPG fetcher interval in hours */
+    long epgFetcherIntervalHour();
+
+    /** Target channel count for EPG. It is used to adjust the EPG length */
+    long epgTargetChannelCount();
+
+    /** Enables fetching a few hours of programs only when the epg is scrolled to that time. */
+    boolean fetchProgramsAsNeeded();
+
+    /** How many hours of programs are loaded in the program guide for during the initial fetch */
+    long programGuideInitialFetchHours();
+
+    /** How many hours of programs are loaded in the program guide */
+    long programGuideMaxHours();
+}
diff --git a/common/src/com/android/tv/common/flags/CloudEpgFlags.java b/common/src/com/android/tv/common/flags/CloudEpgFlags.java
new file mode 100755
index 0000000..ab4c6a1
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/CloudEpgFlags.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.flags;
+
+/** Flags for Cloud EPG */
+public interface CloudEpgFlags {
+
+    /**
+     * Whether or not this feature is compiled into this build.
+     *
+     * <p>This returns true by default, unless the is_compiled_selector parameter was set during
+     * code generation.
+     */
+    boolean compiled();
+
+    /** Is the device in a region supported by Cloud Epg */
+    boolean supportedRegion();
+
+    /** List of input ids that Live TV will update their EPG. */
+    String thirdPartyEpgInputsCsv();
+}
diff --git a/common/src/com/android/tv/common/flags/ConcurrentDvrPlaybackFlags.java b/common/src/com/android/tv/common/flags/ConcurrentDvrPlaybackFlags.java
new file mode 100755
index 0000000..1afff79
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/ConcurrentDvrPlaybackFlags.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.flags;
+
+/** Flags allowing concurrent DVR playback */
+public interface ConcurrentDvrPlaybackFlags {
+
+    /**
+     * Whether or not this feature is compiled into this build.
+     *
+     * <p>This returns true by default, unless the is_compiled_selector parameter was set during
+     * code generation.
+     */
+    boolean compiled();
+
+    /** Enable playback of DVR playback during recording */
+    boolean enabled();
+
+    /** Enable tuner using recording data for playback in onTune */
+    boolean onTuneUsesRecording();
+}
diff --git a/common/src/com/android/tv/common/flags/TunerFlags.java b/common/src/com/android/tv/common/flags/TunerFlags.java
new file mode 100755
index 0000000..5f899b9
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/TunerFlags.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.flags;
+
+/** Flags for tuner */
+public interface TunerFlags {
+
+    /**
+     * Whether or not this feature is compiled into this build.
+     *
+     * <p>This returns true by default, unless the is_compiled_selector parameter was set during
+     * code generation.
+     */
+    boolean compiled();
+
+    /** Tune using current recording if available. */
+    boolean tuneUsingRecording();
+
+    /** Enable using exoplayer V2 */
+    boolean useExoplayerV2();
+}
diff --git a/common/src/com/android/tv/common/flags/UiFlags.java b/common/src/com/android/tv/common/flags/UiFlags.java
new file mode 100755
index 0000000..4c88d08
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/UiFlags.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.flags;
+
+/** Flags for Live TV UI */
+public interface UiFlags {
+
+    /**
+     * Whether or not this feature is compiled into this build.
+     *
+     * <p>This returns true by default, unless the is_compiled_selector parameter was set during
+     * code generation.
+     */
+    boolean compiled();
+
+    /**
+     * Number of days to be shown by Recording History.
+     *
+     * <p>Set to 0 for all recordings.
+     */
+    long maxHistoryDays();
+
+    /** Unhide the launcher all the time */
+    boolean uhideLauncher();
+
+    /** Use the Leanback Pin Picker */
+    boolean useLeanbackPinPicker();
+}
diff --git a/common/src/com/android/tv/common/flags/has/HasCloudEpgFlags.java b/common/src/com/android/tv/common/flags/has/HasCloudEpgFlags.java
new file mode 100644
index 0000000..c33c552
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/has/HasCloudEpgFlags.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.flags.has;
+
+import android.content.Context;
+import com.android.tv.common.flags.CloudEpgFlags;
+
+/** Has {@link CloudEpgFlags} */
+public interface HasCloudEpgFlags {
+
+    static CloudEpgFlags fromContext(Context context) {
+        return ((HasCloudEpgFlags) HasUtils.getApplicationContext(context)).getCloudEpgFlags();
+    }
+
+    CloudEpgFlags getCloudEpgFlags();
+}
diff --git a/common/src/com/android/tv/common/flags/has/HasConcurrentDvrPlaybackFlags.java b/common/src/com/android/tv/common/flags/has/HasConcurrentDvrPlaybackFlags.java
new file mode 100644
index 0000000..b471087
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/has/HasConcurrentDvrPlaybackFlags.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.flags.has;
+
+import android.content.Context;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+
+/** Has {@link ConcurrentDvrPlaybackFlags} */
+public interface HasConcurrentDvrPlaybackFlags {
+
+    static ConcurrentDvrPlaybackFlags fromContext(Context context) {
+        return ((HasConcurrentDvrPlaybackFlags) HasUtils.getApplicationContext(context))
+                .getConcurrentDvrPlaybackFlags();
+    }
+
+    ConcurrentDvrPlaybackFlags getConcurrentDvrPlaybackFlags();
+}
diff --git a/src/com/android/tv/util/Filter.java b/common/src/com/android/tv/common/flags/has/HasUiFlags.java
similarity index 63%
copy from src/com/android/tv/util/Filter.java
copy to common/src/com/android/tv/common/flags/has/HasUiFlags.java
index 3e24a49..72cc84f 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/common/src/com/android/tv/common/flags/has/HasUiFlags.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -11,13 +11,14 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
- * limitations under the License.
+ * limitations under the License
  */
+package com.android.tv.common.flags.has;
 
-package com.android.tv.util;
+import com.android.tv.common.flags.UiFlags;
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
+/** Has {@link UiFlags} */
+public interface HasUiFlags {
+
+    UiFlags getUiFlags();
 }
diff --git a/common/src/com/android/tv/common/flags/has/HasUtils.java b/common/src/com/android/tv/common/flags/has/HasUtils.java
new file mode 100644
index 0000000..1c6126d
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/has/HasUtils.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.flags.has;
+
+import android.content.Context;
+
+/** Static utilities for Has interfaces. */
+public final class HasUtils {
+
+    /** Returns the application context. */
+    public static Context getApplicationContext(Context context) {
+        Context appContext = context.getApplicationContext();
+        return appContext != null ? appContext : context;
+    }
+
+    private HasUtils() {}
+}
diff --git a/common/src/com/android/tv/common/flags/impl/DefaultBackendKnobsFlags.java b/common/src/com/android/tv/common/flags/impl/DefaultBackendKnobsFlags.java
new file mode 100644
index 0000000..a189e47
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/impl/DefaultBackendKnobsFlags.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.flags.impl;
+
+/** Flags for tuning non ui behavior. */
+public final class DefaultBackendKnobsFlags
+        implements com.android.tv.common.flags.BackendKnobsFlags {
+
+    @Override
+    public boolean compiled() {
+        return true;
+    }
+
+    @Override
+    public boolean enablePartialProgramFetch() {
+        return false;
+    }
+
+    @Override
+    public long epgFetcherIntervalHour() {
+        return 25;
+    }
+
+    @Override
+    public boolean fetchProgramsAsNeeded() {
+        return false;
+    }
+
+    @Override
+    public long programGuideInitialFetchHours() {
+        return 8;
+    }
+
+    @Override
+    public long programGuideMaxHours() {
+        return 336;
+    }
+
+    @Override
+    public long epgTargetChannelCount() {
+        return 100;
+    }
+}
diff --git a/common/src/com/android/tv/common/flags/impl/DefaultCloudEpgFlags.java b/common/src/com/android/tv/common/flags/impl/DefaultCloudEpgFlags.java
new file mode 100644
index 0000000..34c4fc4
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/impl/DefaultCloudEpgFlags.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.flags.impl;
+
+import com.android.tv.common.flags.CloudEpgFlags;
+
+/** Default flags for Cloud EPG */
+public final class DefaultCloudEpgFlags implements CloudEpgFlags {
+
+    private String mThirdPartyEpgInputCsv =
+            "com.google.android.tv/.tuner.tvinput.TunerTvInputService,"
+                    + "com.technicolor.skipper.tuner/.tvinput.TunerTvInputService,"
+                    + "com.silicondust.view/.tif.SDTvInputService";
+
+    @Override
+    public boolean compiled() {
+        return true;
+    }
+
+    @Override
+    public boolean supportedRegion() {
+        return false;
+    }
+
+    public void setThirdPartyEpgInputCsv(String value) {
+        mThirdPartyEpgInputCsv = value;
+    }
+
+    @Override
+    public String thirdPartyEpgInputsCsv() {
+        return mThirdPartyEpgInputCsv;
+    }
+}
diff --git a/common/src/com/android/tv/common/flags/impl/DefaultConcurrentDvrPlaybackFlags.java b/common/src/com/android/tv/common/flags/impl/DefaultConcurrentDvrPlaybackFlags.java
new file mode 100644
index 0000000..8d8c584
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/impl/DefaultConcurrentDvrPlaybackFlags.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.flags.impl;
+
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+
+/** Default flags for Concurrent DVR Playback */
+public final class DefaultConcurrentDvrPlaybackFlags implements ConcurrentDvrPlaybackFlags {
+
+    @Override
+    public boolean compiled() {
+        return true;
+    }
+
+    @Override
+    public boolean enabled() {
+        return false;
+    }
+
+    @Override
+    public boolean onTuneUsesRecording() {
+        return false;
+    }
+}
diff --git a/common/src/com/android/tv/common/flags/impl/DefaultFlagsModule.java b/common/src/com/android/tv/common/flags/impl/DefaultFlagsModule.java
new file mode 100644
index 0000000..4935236
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/impl/DefaultFlagsModule.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.flags.impl;
+
+import dagger.Module;
+import dagger.Provides;
+import dagger.Reusable;
+import com.android.tv.common.flags.BackendKnobsFlags;
+import com.android.tv.common.flags.CloudEpgFlags;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+import com.android.tv.common.flags.TunerFlags;
+import com.android.tv.common.flags.UiFlags;
+
+/** Provides default flags. */
+@Module
+public class DefaultFlagsModule {
+
+    @Provides
+    @Reusable
+    BackendKnobsFlags provideBackendKnobsFlags() {
+        return new DefaultBackendKnobsFlags();
+    }
+
+    @Provides
+    @Reusable
+    CloudEpgFlags provideCloudEpgFlags() {
+        return new DefaultCloudEpgFlags();
+    }
+
+    @Provides
+    @Reusable
+    ConcurrentDvrPlaybackFlags provideConcurrentDvrPlaybackFlags() {
+        return new DefaultConcurrentDvrPlaybackFlags();
+    }
+
+    @Provides
+    @Reusable
+    TunerFlags provideTunerFlags() {
+        return new DefaultTunerFlags();
+    }
+
+    @Provides
+    @Reusable
+    UiFlags provideUiFlags() {
+        return new DefaultUiFlags();
+    }
+}
diff --git a/common/src/com/android/tv/common/flags/impl/DefaultTunerFlags.java b/common/src/com/android/tv/common/flags/impl/DefaultTunerFlags.java
new file mode 100644
index 0000000..195953b
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/impl/DefaultTunerFlags.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.flags.impl;
+
+import com.android.tv.common.flags.TunerFlags;
+
+/** Default Flags for Tuner */
+public class DefaultTunerFlags implements TunerFlags {
+
+    @Override
+    public boolean compiled() {
+        return true;
+    }
+
+    @Override
+    public boolean tuneUsingRecording() {
+        return false;
+    }
+
+    @Override
+    public boolean useExoplayerV2() {
+        return false;
+    }
+}
diff --git a/common/src/com/android/tv/common/flags/impl/DefaultUiFlags.java b/common/src/com/android/tv/common/flags/impl/DefaultUiFlags.java
new file mode 100644
index 0000000..fce4585
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/impl/DefaultUiFlags.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.flags.impl;
+
+import com.android.tv.common.flags.UiFlags;
+
+/** Default Flags for Live TV UI */
+public class DefaultUiFlags implements UiFlags {
+
+    @Override
+    public boolean compiled() {
+        return true;
+    }
+
+    @Override
+    public boolean uhideLauncher() {
+        return false;
+    }
+
+    @Override
+    public boolean useLeanbackPinPicker() {
+        return false;
+    }
+
+    @Override
+    public long maxHistoryDays() {
+        return 7;
+    }
+}
diff --git a/common/src/com/android/tv/common/recording/RecordingStorageStatusManager.java b/common/src/com/android/tv/common/recording/RecordingStorageStatusManager.java
index 8b45a73..0fb864b 100644
--- a/common/src/com/android/tv/common/recording/RecordingStorageStatusManager.java
+++ b/common/src/com/android/tv/common/recording/RecordingStorageStatusManager.java
@@ -217,6 +217,7 @@
             }
         } catch (IllegalArgumentException e) {
             // In rare cases, storage status change was not notified yet.
+            Log.w(TAG, "Error getting Dvr Storage Status.", e);
             SoftPreconditions.checkState(false);
             return STORAGE_STATUS_FREE_SPACE_INSUFFICIENT;
         }
@@ -246,7 +247,7 @@
                 StatFs statFs = new StatFs(storageMountedDir.toString());
                 storageMountedCapacity = statFs.getTotalBytes();
             } catch (IllegalArgumentException e) {
-                Log.e(TAG, "Storage mount status was changed.");
+                Log.w(TAG, "Storage mount status was changed.", e);
                 storageMounted = false;
                 storageMountedDir = null;
             }
diff --git a/common/src/com/android/tv/common/singletons/HasSingletons.java b/common/src/com/android/tv/common/singletons/HasSingletons.java
new file mode 100644
index 0000000..193aed3
--- /dev/null
+++ b/common/src/com/android/tv/common/singletons/HasSingletons.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.singletons;
+
+import android.content.Context;
+
+/**
+ * A type that can know about and supply a singleton, typically a type t such as an android activity
+ * or application.
+ */
+public interface HasSingletons<C> {
+
+    @SuppressWarnings("unchecked") // injection
+    static <C> C get(Class<C> clazz, Context context) {
+        return ((HasSingletons<C>) context).singletons();
+    }
+
+    /** Returns the strongly typed singleton. */
+    C singletons();
+}
diff --git a/src/com/android/tv/util/Filter.java b/common/src/com/android/tv/common/singletons/HasTvInputId.java
similarity index 64%
copy from src/com/android/tv/util/Filter.java
copy to common/src/com/android/tv/common/singletons/HasTvInputId.java
index 3e24a49..4bc0a21 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/common/src/com/android/tv/common/singletons/HasTvInputId.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,11 +13,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package com.android.tv.common.singletons;
 
-package com.android.tv.util;
+/**
+ * Has TunerInputId.
+ *
+ * <p>This is used buy both the tuner to get its input id and by the Live TV to get the
+ * embedded tuner input id.
+ */
+public interface HasTvInputId {
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
+    String getEmbeddedTunerInputId();
 }
diff --git a/common/src/com/android/tv/common/support/README.md b/common/src/com/android/tv/common/support/README.md
new file mode 100644
index 0000000..67993f3
--- /dev/null
+++ b/common/src/com/android/tv/common/support/README.md
@@ -0,0 +1,8 @@
+# Support Libraries
+
+Packages here are destined to become support libraries.
+
+Each package should be self contained and only have dependencies on public libraries.
+
+It if becomes clear a package should not or will not be part of a support library move it to a 
+different location.
\ No newline at end of file
diff --git a/common/src/com/android/tv/common/support/tis/BaseTvInputService.java b/common/src/com/android/tv/common/support/tis/BaseTvInputService.java
new file mode 100644
index 0000000..7791550
--- /dev/null
+++ b/common/src/com/android/tv/common/support/tis/BaseTvInputService.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.support.tis;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.tv.TvInputManager;
+import android.media.tv.TvInputService;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import com.android.tv.common.support.tis.TifSession.TifSessionFactory;
+
+/** Abstract TVInputService. */
+public abstract class BaseTvInputService extends TvInputService {
+
+    private static final IntentFilter INTENT_FILTER = new IntentFilter();
+
+    static {
+        INTENT_FILTER.addAction(TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED);
+        INTENT_FILTER.addAction(TvInputManager.ACTION_BLOCKED_RATINGS_CHANGED);
+    }
+
+    @VisibleForTesting
+    protected final BroadcastReceiver broadcastReceiver =
+            new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    switch (intent.getAction()) {
+                        case TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED:
+                        case TvInputManager.ACTION_BLOCKED_RATINGS_CHANGED:
+                            for (Session session : getSessionManager().getSessions()) {
+                                if (session instanceof WrappedSession) {
+                                    ((WrappedSession) session).onParentalControlsChanged();
+                                }
+                            }
+                            break;
+                        default:
+                            // do nothing
+                    }
+                }
+            };
+
+    @Nullable
+    @Override
+    public final WrappedSession onCreateSession(String inputId) {
+        SessionManager sessionManager = getSessionManager();
+        if (sessionManager.canCreateNewSession()) {
+            WrappedSession session =
+                    new WrappedSession(
+                            getApplicationContext(),
+                            sessionManager,
+                            getTifSessionFactory(),
+                            inputId);
+            sessionManager.addSession(session);
+            return session;
+        }
+        return null;
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        registerReceiver(broadcastReceiver, INTENT_FILTER);
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        unregisterReceiver(broadcastReceiver);
+    }
+
+    protected abstract TifSessionFactory getTifSessionFactory();
+
+    protected abstract SessionManager getSessionManager();
+}
diff --git a/common/src/com/android/tv/common/support/tis/SessionManager.java b/common/src/com/android/tv/common/support/tis/SessionManager.java
new file mode 100644
index 0000000..5eeebc8
--- /dev/null
+++ b/common/src/com/android/tv/common/support/tis/SessionManager.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.support.tis;
+
+import android.media.tv.TvInputService.Session;
+import com.google.common.collect.ImmutableSet;
+
+/** Manages the number of concurrent sessions, keeping track of when sessions are released. */
+public interface SessionManager {
+
+    void removeSession(Session session);
+
+    void addSession(Session session);
+
+    boolean canCreateNewSession();
+
+    ImmutableSet<Session> getSessions();
+}
diff --git a/common/src/com/android/tv/common/support/tis/SimpleSessionManager.java b/common/src/com/android/tv/common/support/tis/SimpleSessionManager.java
new file mode 100644
index 0000000..f0636cc
--- /dev/null
+++ b/common/src/com/android/tv/common/support/tis/SimpleSessionManager.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.support.tis;
+
+import android.media.tv.TvInputService.Session;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.VisibleForTesting;
+import android.util.ArraySet;
+import com.google.common.collect.ImmutableSet;
+import java.util.HashSet;
+import java.util.Set;
+
+/** A simple session manager that allows a maximum number of concurrent session. */
+public final class SimpleSessionManager implements SessionManager {
+
+    private final Set<Session> sessions;
+    private final int max;
+
+    public SimpleSessionManager(int max) {
+        this.max = max;
+        sessions = VERSION.SDK_INT >= VERSION_CODES.M ? new ArraySet<>() : new HashSet<>();
+    }
+
+    @Override
+    public void removeSession(Session session) {
+        sessions.remove(session);
+    }
+
+    @Override
+    public void addSession(Session session) {
+        sessions.add(session);
+    }
+
+    @Override
+    public boolean canCreateNewSession() {
+        return sessions.size() < max;
+    }
+
+    @Override
+    public ImmutableSet<Session> getSessions() {
+        return ImmutableSet.copyOf(sessions);
+    }
+
+    @VisibleForTesting
+    int getSessionCount() {
+        return sessions.size();
+    }
+}
diff --git a/common/src/com/android/tv/common/support/tis/TifSession.java b/common/src/com/android/tv/common/support/tis/TifSession.java
new file mode 100644
index 0000000..61cfe76
--- /dev/null
+++ b/common/src/com/android/tv/common/support/tis/TifSession.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.support.tis;
+
+import android.annotation.TargetApi;
+import android.media.PlaybackParams;
+import android.media.tv.TvContentRating;
+import android.media.tv.TvInputManager;
+import android.media.tv.TvInputService.Session;
+import android.media.tv.TvTrackInfo;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.FloatRange;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.Surface;
+import android.view.View;
+import java.util.List;
+
+/**
+ * Custom {@link android.media.tv.TvInputService.Session} class that uses delegation and a callback
+ * to separate it from the TvInputService for easier testing.
+ */
+public abstract class TifSession {
+
+    private final TifSessionCallbacks callback;
+
+    /**
+     * Creates TV Input Framework Session with the given callback.
+     *
+     * <p>The callback is used to pass notification to the actual {@link
+     * android.media.tv.TvInputService.Session}.
+     *
+     * <p>Pass a mock callback for tests.
+     */
+    protected TifSession(TifSessionCallbacks callback) {
+        this.callback = callback;
+    }
+
+    /**
+     * Called after this session had been created and the callback is attached.
+     *
+     * <p>Do not call notify methods in the constructor, instead call them here if needed at
+     * creation time. eg @{@link Session#notifyTimeShiftStatusChanged(int)}.
+     */
+    public void onSessionCreated() {}
+
+    /** @see Session#onRelease() */
+    public void onRelease() {}
+
+    /** @see Session#onSetSurface(Surface) */
+    public abstract boolean onSetSurface(@Nullable Surface surface);
+
+    /** @see Session#onSurfaceChanged(int, int, int) */
+    public abstract void onSurfaceChanged(int format, int width, int height);
+
+    /** @see Session#onSetStreamVolume(float) */
+    public abstract void onSetStreamVolume(@FloatRange(from = 0.0, to = 1.0) float volume);
+
+    /** @see Session#onTune(Uri) */
+    public abstract boolean onTune(Uri channelUri);
+
+    /** @see Session#onSetCaptionEnabled(boolean) */
+    public abstract void onSetCaptionEnabled(boolean enabled);
+
+    /** @see Session#onUnblockContent(TvContentRating) */
+    public abstract void onUnblockContent(TvContentRating unblockedRating);
+
+    /** @see Session#onTimeShiftGetCurrentPosition() */
+    @TargetApi(Build.VERSION_CODES.M)
+    public long onTimeShiftGetCurrentPosition() {
+        return TvInputManager.TIME_SHIFT_INVALID_TIME;
+    }
+
+    /** @see Session#onTimeShiftGetStartPosition() */
+    @TargetApi(Build.VERSION_CODES.M)
+    public long onTimeShiftGetStartPosition() {
+        return TvInputManager.TIME_SHIFT_INVALID_TIME;
+    }
+
+    /** @see Session#onTimeShiftPause() */
+    @TargetApi(Build.VERSION_CODES.M)
+    public void onTimeShiftPause() {}
+
+    /** @see Session#onTimeShiftResume() */
+    @TargetApi(Build.VERSION_CODES.M)
+    public void onTimeShiftResume() {}
+
+    /** @see Session#onTimeShiftSeekTo(long) */
+    @TargetApi(Build.VERSION_CODES.M)
+    public void onTimeShiftSeekTo(long timeMs) {}
+
+    /** @see Session#onTimeShiftSetPlaybackParams(PlaybackParams) */
+    @TargetApi(Build.VERSION_CODES.M)
+    public void onTimeShiftSetPlaybackParams(PlaybackParams params) {}
+
+    public void onParentalControlsChanged() {}
+
+    /** @see Session#notifyChannelRetuned(Uri) */
+    public final void notifyChannelRetuned(final Uri channelUri) {
+        callback.notifyChannelRetuned(channelUri);
+    }
+
+    /** @see Session#notifyTracksChanged(List) */
+    public final void notifyTracksChanged(final List<TvTrackInfo> tracks) {
+        callback.notifyTracksChanged(tracks);
+    }
+
+    /** @see Session#notifyTrackSelected(int, String) */
+    public final void notifyTrackSelected(final int type, final String trackId) {
+        callback.notifyTrackSelected(type, trackId);
+    }
+
+    /** @see Session#notifyVideoAvailable() */
+    public final void notifyVideoAvailable() {
+        callback.notifyVideoAvailable();
+    }
+
+    /** @see Session#notifyVideoUnavailable(int) */
+    public final void notifyVideoUnavailable(final int reason) {
+        callback.notifyVideoUnavailable(reason);
+    }
+
+    /** @see Session#notifyContentAllowed() */
+    public final void notifyContentAllowed() {
+        callback.notifyContentAllowed();
+    }
+
+    /** @see Session#notifyContentBlocked(TvContentRating) */
+    public final void notifyContentBlocked(@NonNull final TvContentRating rating) {
+        callback.notifyContentBlocked(rating);
+    }
+
+    /** @see Session#notifyTimeShiftStatusChanged(int) */
+    @TargetApi(VERSION_CODES.M)
+    public final void notifyTimeShiftStatusChanged(final int status) {
+        callback.notifyTimeShiftStatusChanged(status);
+    }
+
+    /** @see Session#setOverlayViewEnabled(boolean) */
+    public void setOverlayViewEnabled(boolean enabled) {
+        callback.setOverlayViewEnabled(enabled);
+    }
+
+    /** @see Session#onCreateOverlayView() */
+    public View onCreateOverlayView() {
+        return null;
+    }
+
+    /** @see Session#onOverlayViewSizeChanged(int, int) */
+    public void onOverlayViewSizeChanged(int width, int height) {}
+
+    /**
+     * Callbacks used to notify the {@link android.media.tv.TvInputService.Session}.
+     *
+     * <p>This is implemented internally by {@link WrappedSession}, and can be mocked for tests.
+     */
+    public interface TifSessionCallbacks {
+        /** @see Session#notifyChannelRetuned(Uri) */
+        void notifyChannelRetuned(final Uri channelUri);
+        /** @see Session#notifyTracksChanged(List) */
+        void notifyTracksChanged(final List<TvTrackInfo> tracks);
+        /** @see Session#notifyTrackSelected(int, String) */
+        void notifyTrackSelected(final int type, final String trackId);
+        /** @see Session#notifyVideoAvailable() */
+        void notifyVideoAvailable();
+        /** @see Session#notifyVideoUnavailable(int) */
+        void notifyVideoUnavailable(final int reason);
+        /** @see Session#notifyContentAllowed() */
+        void notifyContentAllowed();
+        /** @see Session#notifyContentBlocked(TvContentRating) */
+        void notifyContentBlocked(@NonNull final TvContentRating rating);
+        /** @see Session#notifyTimeShiftStatusChanged(int) */
+        @TargetApi(VERSION_CODES.M)
+        void notifyTimeShiftStatusChanged(final int status);
+        /** @see Session#setOverlayViewEnabled(boolean) */
+        void setOverlayViewEnabled(boolean enabled);
+    }
+
+    /**
+     * Creates a {@link TifSession}.
+     *
+     * <p>This is used by {@link WrappedSession} to create the desired {@code TifSession}. Should be
+     * used with <a href="http://go/autofactory">go/autofactory</a>.
+     */
+    public interface TifSessionFactory {
+        TifSession create(TifSessionCallbacks callbacks, String inputId);
+    }
+}
diff --git a/common/src/com/android/tv/common/support/tis/WrappedSession.java b/common/src/com/android/tv/common/support/tis/WrappedSession.java
new file mode 100644
index 0000000..f4a71dd
--- /dev/null
+++ b/common/src/com/android/tv/common/support/tis/WrappedSession.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.support.tis;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.media.PlaybackParams;
+import android.media.tv.TvContentRating;
+import android.media.tv.TvInputService.Session;
+import android.net.Uri;
+import android.os.Build;
+import android.support.annotation.FloatRange;
+import android.support.annotation.Nullable;
+import android.view.Surface;
+import android.view.View;
+import com.android.tv.common.support.tis.TifSession.TifSessionCallbacks;
+import com.android.tv.common.support.tis.TifSession.TifSessionFactory;
+
+/**
+ * Delegates all call to a {@link TifSession} and removes the session from the {@link
+ * SessionManager} when {@link Session#onRelease()} is called.
+ */
+final class WrappedSession extends Session implements TifSessionCallbacks {
+
+    private final SessionManager listener;
+    private final TifSession delegate;
+
+    WrappedSession(
+            Context context,
+            SessionManager sessionManager,
+            TifSessionFactory sessionFactory,
+            String inputId) {
+        super(context);
+        this.listener = sessionManager;
+        this.delegate = sessionFactory.create(this, inputId);
+    }
+
+    @Override
+    public void onRelease() {
+        delegate.onRelease();
+        listener.removeSession(this);
+    }
+
+    @Override
+    public boolean onSetSurface(@Nullable Surface surface) {
+        return delegate.onSetSurface(surface);
+    }
+
+    @Override
+    public void onSurfaceChanged(int format, int width, int height) {
+        delegate.onSurfaceChanged(format, width, height);
+    }
+
+    @Override
+    public void onSetStreamVolume(@FloatRange(from = 0.0, to = 1.0) float volume) {
+        delegate.onSetStreamVolume(volume);
+    }
+
+    @Override
+    public boolean onTune(Uri channelUri) {
+        return delegate.onTune(channelUri);
+    }
+
+    @Override
+    public void onSetCaptionEnabled(boolean enabled) {
+        delegate.onSetCaptionEnabled(enabled);
+    }
+
+    @Override
+    public void onUnblockContent(TvContentRating unblockedRating) {
+        delegate.onUnblockContent(unblockedRating);
+    }
+
+    @Override
+    @TargetApi(Build.VERSION_CODES.M)
+    public long onTimeShiftGetCurrentPosition() {
+        return delegate.onTimeShiftGetCurrentPosition();
+    }
+
+    @Override
+    @TargetApi(Build.VERSION_CODES.M)
+    public long onTimeShiftGetStartPosition() {
+        return delegate.onTimeShiftGetStartPosition();
+    }
+
+    @Override
+    @TargetApi(Build.VERSION_CODES.M)
+    public void onTimeShiftPause() {
+        delegate.onTimeShiftPause();
+    }
+
+    @Override
+    @TargetApi(Build.VERSION_CODES.M)
+    public void onTimeShiftResume() {
+        delegate.onTimeShiftResume();
+    }
+
+    @Override
+    @TargetApi(Build.VERSION_CODES.M)
+    public void onTimeShiftSeekTo(long timeMs) {
+        delegate.onTimeShiftSeekTo(timeMs);
+    }
+
+    @Override
+    @TargetApi(Build.VERSION_CODES.M)
+    public void onTimeShiftSetPlaybackParams(PlaybackParams params) {
+        delegate.onTimeShiftSetPlaybackParams(params);
+    }
+
+    public void onParentalControlsChanged() {
+        delegate.onParentalControlsChanged();
+    }
+
+    @Override
+    @TargetApi(Build.VERSION_CODES.M)
+    public void notifyTimeShiftStatusChanged(int status) {
+        // TODO(nchalko): why is the required for call from TisSession.onSessionCreated to work
+        super.notifyTimeShiftStatusChanged(status);
+    }
+
+    @Override
+    public void setOverlayViewEnabled(boolean enabled) {
+        super.setOverlayViewEnabled(enabled);
+    }
+
+    @Override
+    public View onCreateOverlayView() {
+        return delegate.onCreateOverlayView();
+    }
+
+    @Override
+    public void onOverlayViewSizeChanged(int width, int height) {
+        delegate.onOverlayViewSizeChanged(width, height);
+    }
+}
diff --git a/common/src/com/android/tv/common/ui/setup/SetupActivity.java b/common/src/com/android/tv/common/ui/setup/SetupActivity.java
index 67418ce..1a3ddbd 100644
--- a/common/src/com/android/tv/common/ui/setup/SetupActivity.java
+++ b/common/src/com/android/tv/common/ui/setup/SetupActivity.java
@@ -16,7 +16,6 @@
 
 package com.android.tv.common.ui.setup;
 
-import android.app.Activity;
 import android.app.Fragment;
 import android.app.FragmentTransaction;
 import android.os.Bundle;
@@ -27,10 +26,10 @@
 import android.transition.Transition;
 import android.transition.TransitionInflater;
 import android.view.View;
-import android.view.ViewTreeObserver.OnPreDrawListener;
 import com.android.tv.common.R;
 import com.android.tv.common.WeakHandler;
 import com.android.tv.common.ui.setup.animation.SetupAnimationHelper;
+import dagger.android.DaggerActivity;
 
 /**
  * Setup activity for onboarding screens or TIS.
@@ -38,7 +37,7 @@
  * <p>The inherited class should add theme {@code Theme.Setup.GuidedStep} to its definition in
  * AndroidManifest.xml.
  */
-public abstract class SetupActivity extends Activity implements OnActionClickListener {
+public abstract class SetupActivity extends DaggerActivity implements OnActionClickListener {
     private static final int MSG_EXECUTE_ACTION = 1;
 
     private boolean mShowInitialFragment = true;
@@ -55,23 +54,7 @@
         // Show initial fragment only when the saved state is not restored, because the last
         // fragment is restored if savesInstanceState is not null.
         if (savedInstanceState == null) {
-            // This is the workaround to show the first fragment with delay to show the fragment
-            // enter transition. See http://b/26255145
-            getWindow()
-                    .getDecorView()
-                    .getViewTreeObserver()
-                    .addOnPreDrawListener(
-                            new OnPreDrawListener() {
-                                @Override
-                                public boolean onPreDraw() {
-                                    getWindow()
-                                            .getDecorView()
-                                            .getViewTreeObserver()
-                                            .removeOnPreDrawListener(this);
-                                    showInitialFragment();
-                                    return true;
-                                }
-                            });
+            showInitialFragment();
         } else {
             mShowInitialFragment = false;
         }
diff --git a/common/src/com/android/tv/common/util/CommonUtils.java b/common/src/com/android/tv/common/util/CommonUtils.java
index 305431d..4513a87 100644
--- a/common/src/com/android/tv/common/util/CommonUtils.java
+++ b/common/src/com/android/tv/common/util/CommonUtils.java
@@ -138,14 +138,23 @@
         return ISO_8601.get().format(new Date(timeMillis));
     }
 
-    /** Deletes a file or a directory. */
-    public static void deleteDirOrFile(File fileOrDirectory) {
+    /**
+     * Deletes a file or a directory.
+     *
+     * @return <code>true</code> if and only if the file or directory is successfully deleted;
+     *     <code>false</code> otherwise
+     */
+    public static boolean deleteDirOrFile(File fileOrDirectory) {
         if (fileOrDirectory.isDirectory()) {
-            for (File child : fileOrDirectory.listFiles()) {
-                deleteDirOrFile(child);
+            File[] files = fileOrDirectory.listFiles();
+            if (files != null) {
+                for (File child : files) {
+                    deleteDirOrFile(child);
+                }
             }
         }
-        fileOrDirectory.delete();
+        // If earlier deletes failed this will also
+        return fileOrDirectory.delete();
     }
 
     public static boolean isRoboTest() {
diff --git a/common/src/com/android/tv/common/util/LocationUtils.java b/common/src/com/android/tv/common/util/LocationUtils.java
index 5315529..ee5119e 100644
--- a/common/src/com/android/tv/common/util/LocationUtils.java
+++ b/common/src/com/android/tv/common/util/LocationUtils.java
@@ -34,14 +34,20 @@
 
 
 import java.io.IOException;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
+import java.util.Set;
 
 /** A utility class to get the current location. */
 public class LocationUtils {
     private static final String TAG = "LocationUtils";
     private static final boolean DEBUG = false;
 
+    private static final Set<OnUpdateAddressListener> sOnUpdateAddressListeners =
+            Collections.synchronizedSet(new HashSet<>());
+
     private static Context sApplicationContext;
     private static Address sAddress;
     private static String sCountry;
@@ -63,6 +69,39 @@
         return null;
     }
 
+    /** The listener used when address is updated. */
+    public interface OnUpdateAddressListener {
+        /**
+         * Called when address is updated.
+         *
+         * This listener is removed when this method returns true.
+         *
+         * @return {@code true} if the job has been finished and the listener needs to be removed;
+         *         {@code false} otherwise.
+         */
+        boolean onUpdateAddress(Address address);
+    }
+
+    /**
+     * Add an {@link OnUpdateAddressListener} instance.
+     *
+     * Note that the listener is removed automatically when
+     * {@link OnUpdateAddressListener#onUpdateAddress(Address)} is called and returns {@code true}.
+     */
+    public static void addOnUpdateAddressListener(OnUpdateAddressListener listener) {
+        sOnUpdateAddressListeners.add(listener);
+    }
+
+    /**
+     * Remove an {@link OnUpdateAddressListener} instance if it exists.
+     *
+     * Note that the listener will be removed automatically when
+     * {@link OnUpdateAddressListener#onUpdateAddress(Address)} is called and returns {@code true}.
+     */
+    public static void removeOnUpdateAddressListener(OnUpdateAddressListener listener) {
+        sOnUpdateAddressListeners.remove(listener);
+    }
+
     /** Returns the current country. */
     @NonNull
     public static synchronized String getCurrentCountry(Context context) {
@@ -92,6 +131,17 @@
                 } catch (Exception e) {
                     // Do nothing
                 }
+                Set<OnUpdateAddressListener> listenersToRemove = new HashSet<>();
+                synchronized (sOnUpdateAddressListeners) {
+                    for (OnUpdateAddressListener listener : sOnUpdateAddressListeners) {
+                        if (listener.onUpdateAddress(sAddress)) {
+                            listenersToRemove.add(listener);
+                        }
+                    }
+                    for (OnUpdateAddressListener listener : listenersToRemove) {
+                        removeOnUpdateAddressListener(listener);
+                    }
+                }
             } else {
                 if (DEBUG) Log.d(TAG, "No address returned");
             }
diff --git a/common/src/com/android/tv/common/util/NetworkTrafficTags.java b/common/src/com/android/tv/common/util/NetworkTrafficTags.java
index 91f2bcd..3c94aed 100644
--- a/common/src/com/android/tv/common/util/NetworkTrafficTags.java
+++ b/common/src/com/android/tv/common/util/NetworkTrafficTags.java
@@ -43,19 +43,16 @@
 
         @Override
         public void execute(final @NonNull Runnable command) {
-            // TODO(b/62038127): robolectric does not support lamdas in unbundled apps
-            delegateExecutor.execute(
-                    new Runnable() {
-                        @Override
-                        public void run() {
-                            TrafficStats.setThreadStatsTag(tag);
-                            try {
-                                command.run();
-                            } finally {
-                                TrafficStats.clearThreadStatsTag();
-                            }
-                        }
-                    });
+      // TODO(b/62038127): robolectric does not support lamdas in unbundled apps
+      delegateExecutor.execute(
+          () -> {
+            TrafficStats.setThreadStatsTag(tag);
+            try {
+              command.run();
+            } finally {
+              TrafficStats.clearThreadStatsTag();
+            }
+          });
         }
     }
 
diff --git a/common/src/com/android/tv/common/util/PermissionUtils.java b/common/src/com/android/tv/common/util/PermissionUtils.java
index 8d409e5..ca1abdc 100644
--- a/common/src/com/android/tv/common/util/PermissionUtils.java
+++ b/common/src/com/android/tv/common/util/PermissionUtils.java
@@ -65,4 +65,9 @@
         return context.checkSelfPermission("android.permission.INTERNET")
                 == PackageManager.PERMISSION_GRANTED;
     }
+
+    public static boolean hasWriteExternalStorage(Context context) {
+        return context.checkSelfPermission("android.permission.WRITE_EXTERNAL_STORAGE")
+                == PackageManager.PERMISSION_GRANTED;
+    }
 }
diff --git a/common/src/com/android/tv/common/util/StringUtils.java b/common/src/com/android/tv/common/util/StringUtils.java
index b946142..bc82620 100644
--- a/common/src/com/android/tv/common/util/StringUtils.java
+++ b/common/src/com/android/tv/common/util/StringUtils.java
@@ -31,4 +31,9 @@
         }
         return a.compareTo(b);
     }
+
+    /** Returns {@code s} or {@code ""} if {@code s} is {@code null} */
+    public static final String nullToEmpty(String s) {
+        return s == null ? "" : s;
+    }
 }
diff --git a/common/src/com/android/tv/common/util/SystemProperties.java b/common/src/com/android/tv/common/util/SystemProperties.java
index a9f18d4..6ac2907 100644
--- a/common/src/com/android/tv/common/util/SystemProperties.java
+++ b/common/src/com/android/tv/common/util/SystemProperties.java
@@ -40,6 +40,10 @@
     public static final BooleanSystemProperty USE_TRACKER =
             new BooleanSystemProperty("tv_use_tracker", true);
 
+    /** Allow third party inputs. */
+    public static final BooleanSystemProperty ALLOW_THIRD_PARTY_INPUTS =
+            new BooleanSystemProperty("ro.tv_allow_third_party_inputs", true);
+
     static {
         updateSystemProperties();
     }
diff --git a/jni/gen_jni.sh b/gradle.properties
old mode 100755
new mode 100644
similarity index 64%
rename from jni/gen_jni.sh
rename to gradle.properties
index c06b7b9..6208234
--- a/jni/gen_jni.sh
+++ b/gradle.properties
@@ -1,6 +1,4 @@
-#!/bin/bash
-#
-# Copyright (C) 2017 The Android Open Source Project
+# Copyright (C) 2018 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -14,5 +12,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+#
+# Experimental gradle configuration.  This file may not be up to date.
+#
 
-javah -jni -classpath ../../bin/classes:../../../../../../prebuilts/sdk/current/public/android.jar -o tunertvinput_jni.h com.android.tv.tuner.TunerHal
+
+org.gradle.jvmargs=-Xmx6144m -XX:MaxPermSize=6144m -XX:+HeapDumpOnOutOfMemoryError
+
+org.gradle.daemon=true
+org.gradle.parallel=true
+org.gradle.configureondemand=true
\ No newline at end of file
diff --git a/jni/Android.bp b/jni/Android.bp
new file mode 100644
index 0000000..bbf2778
--- /dev/null
+++ b/jni/Android.bp
@@ -0,0 +1,30 @@
+//
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+cc_library_shared {
+    name: "libtunertvinput_jni",
+    srcs: [
+        "tunertvinput_jni.cpp",
+        "DvbManager.cpp",
+    ],
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
+    sdk_version: "23",
+    stl: "c++_static",
+    shared_libs: ["liblog"],
+}
diff --git a/jni/Android.mk b/jni/Android.mk
deleted file mode 100644
index cfc8623..0000000
--- a/jni/Android.mk
+++ /dev/null
@@ -1,30 +0,0 @@
-#
-# Copyright (C) 2017 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-LOCAL_PATH := $(call my-dir)
-
-# --------------------------------------------------------------
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := libtunertvinput_jni
-LOCAL_SRC_FILES += tunertvinput_jni.cpp DvbManager.cpp
-LOCAL_CFLAGS := -Wall -Werror
-LOCAL_SDK_VERSION := 23
-LOCAL_NDK_STL_VARIANT := c++_static
-LOCAL_LDLIBS := -llog
-
-include $(BUILD_SHARED_LIBRARY)
-include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/jni/DvbManager.cpp b/jni/DvbManager.cpp
index 8e51999..f9dff59 100644
--- a/jni/DvbManager.cpp
+++ b/jni/DvbManager.cpp
@@ -82,6 +82,37 @@
     return false;
 }
 
+// This function gets the signal strength from tuner.
+// Output can be:
+// -3 means File Descriptor invalid,
+//    or DVB version is not supported,
+//    or ERROR while communicate with hardware via ioctl.
+// int signal returns the raw signal strength value.
+int DvbManager::getSignalStrength() {
+    // TODO(b/74197177): add support for DVB V5.
+    if (mFeFd == -1 || mDvbApiVersion != DVB_API_VERSION3) {
+        return -3;
+    }
+    uint16_t strength = 0;
+    // ERROR code from ioctl can be:
+    // EBADF means fd is not a valid open file descriptor
+    // EFAULT means status points to invalid address
+    // ENOSIGNAL means there is no signal, thus no meaningful signal strength
+    // ENOSYS means function not available for this device
+    //
+    // The function used to communicate with tuner in DVB v3 is
+    // ioctl(fd, request, &strength)
+    // int fd is the File Descriptor, can't be -1
+    // int request is the request type,
+    // FE_READ_SIGNAL_STRENGTH for getting signal strength
+    // uint16_t *strength stores the strength value returned from tuner
+    if (ioctl(mFeFd, FE_READ_SIGNAL_STRENGTH, &strength) == -1) {
+        ALOGD("FE_READ_SIGNAL_STRENGTH failed, %s", strerror(errno));
+        return -3;
+    }
+    return strength;
+}
+
 int DvbManager::tune(JNIEnv *env, jobject thiz,
         const int frequency, const char *modulationStr, int timeout_ms) {
     resetExceptFe();
diff --git a/jni/DvbManager.h b/jni/DvbManager.h
index 124fa94..b01113e 100644
--- a/jni/DvbManager.h
+++ b/jni/DvbManager.h
@@ -49,7 +49,7 @@
     static const int DELIVERY_SYSTEM_ATSC =
         com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_ATSC;
     static const int DELIVERY_SYSTEM_DVBC =
-        com_android_tv_tuner_TunerHal_DDELIVERY_SYSTEM_DVBC;
+        com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBC;
     static const int DELIVERY_SYSTEM_DVBS =
         com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBS;
     static const int DELIVERY_SYSTEM_DVBS2 =
@@ -85,6 +85,7 @@
     void closeAllDvbPidFilter();
     void setHasPendingTune(bool hasPendingTune);
     int getDeliverySystemType(JNIEnv *env, jobject thiz);
+    int getSignalStrength();
 
 private:
     int openDvbFe(JNIEnv *env, jobject thiz);
diff --git a/jni/tunertvinput_jni.cpp b/jni/tunertvinput_jni.cpp
index 368e2d5..030f961 100644
--- a/jni/tunertvinput_jni.cpp
+++ b/jni/tunertvinput_jni.cpp
@@ -96,6 +96,23 @@
 
 /*
  * Class:     com_android_tv_tuner_TunerHal
+ * Method:    nativeGetSignalStrength
+ * Signature: (J)V
+ */
+JNIEXPORT int JNICALL
+Java_com_android_tv_tuner_TunerHal_nativeGetSignalStrength(
+    JNIEnv *, jobject, jlong deviceId) {
+  std::map<jlong, DvbManager *>::iterator it = sDvbManagers.find(deviceId);
+  if (it != sDvbManagers.end()) {
+    return it->second->getSignalStrength();
+  }
+  // If DvbManager can't be found,
+  // return -3 as signal strength not supported.
+  return -3;
+}
+
+/*
+ * Class:     com_android_tv_tuner_TunerHal
  * Method:    nativeAddPidFilter
  * Signature: (JII)V
  */
diff --git a/jni/tunertvinput_jni.h b/jni/tunertvinput_jni.h
old mode 100644
new mode 100755
index d299c30..36e631f
--- a/jni/tunertvinput_jni.h
+++ b/jni/tunertvinput_jni.h
@@ -17,20 +17,12 @@
 #define com_android_tv_tuner_TunerHal_FILTER_TYPE_VIDEO 2L
 #undef com_android_tv_tuner_TunerHal_FILTER_TYPE_PCR
 #define com_android_tv_tuner_TunerHal_FILTER_TYPE_PCR 3L
-#undef com_android_tv_tuner_TunerHal_PID_PAT
-#define com_android_tv_tuner_TunerHal_PID_PAT 0L
-#undef com_android_tv_tuner_TunerHal_PID_ATSC_SI_BASE
-#define com_android_tv_tuner_TunerHal_PID_ATSC_SI_BASE 8187L
-#undef com_android_tv_tuner_TunerHal_DEFAULT_VSB_TUNE_TIMEOUT_MS
-#define com_android_tv_tuner_TunerHal_DEFAULT_VSB_TUNE_TIMEOUT_MS 2000L
-#undef com_android_tv_tuner_TunerHal_DEFAULT_QAM_TUNE_TIMEOUT_MS
-#define com_android_tv_tuner_TunerHal_DEFAULT_QAM_TUNE_TIMEOUT_MS 4000L
 #undef com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_UNDEFINED
 #define com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_UNDEFINED 0L
 #undef com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_ATSC
 #define com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_ATSC 1L
 #undef com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBC
-#define com_android_tv_tuner_TunerHal_DDELIVERY_SYSTEM_DVBC 2L
+#define com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBC 2L
 #undef com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBS
 #define com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBS 3L
 #undef com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBS2
@@ -39,29 +31,51 @@
 #define com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBT 5L
 #undef com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBT2
 #define com_android_tv_tuner_TunerHal_DELIVERY_SYSTEM_DVBT2 6L
+#undef com_android_tv_tuner_TunerHal_TUNER_TYPE_BUILT_IN
+#define com_android_tv_tuner_TunerHal_TUNER_TYPE_BUILT_IN 1L
+#undef com_android_tv_tuner_TunerHal_TUNER_TYPE_USB
+#define com_android_tv_tuner_TunerHal_TUNER_TYPE_USB 2L
+#undef com_android_tv_tuner_TunerHal_TUNER_TYPE_NETWORK
+#define com_android_tv_tuner_TunerHal_TUNER_TYPE_NETWORK 3L
+#undef com_android_tv_tuner_TunerHal_PID_PAT
+#define com_android_tv_tuner_TunerHal_PID_PAT 0L
+#undef com_android_tv_tuner_TunerHal_PID_ATSC_SI_BASE
+#define com_android_tv_tuner_TunerHal_PID_ATSC_SI_BASE 8187L
+#undef com_android_tv_tuner_TunerHal_PID_DVB_SDT
+#define com_android_tv_tuner_TunerHal_PID_DVB_SDT 17L
+#undef com_android_tv_tuner_TunerHal_PID_DVB_EIT
+#define com_android_tv_tuner_TunerHal_PID_DVB_EIT 18L
+#undef com_android_tv_tuner_TunerHal_DEFAULT_VSB_TUNE_TIMEOUT_MS
+#define com_android_tv_tuner_TunerHal_DEFAULT_VSB_TUNE_TIMEOUT_MS 2000L
+#undef com_android_tv_tuner_TunerHal_DEFAULT_QAM_TUNE_TIMEOUT_MS
+#define com_android_tv_tuner_TunerHal_DEFAULT_QAM_TUNE_TIMEOUT_MS 4000L
+#undef com_android_tv_tuner_TunerHal_BUILT_IN_TUNER_TYPE_LINUX_DVB
+#define com_android_tv_tuner_TunerHal_BUILT_IN_TUNER_TYPE_LINUX_DVB 1L
+#undef com_android_tv_tuner_TunerHal_BUILT_IN_TUNER_TYPE_ARCHER
+#define com_android_tv_tuner_TunerHal_BUILT_IN_TUNER_TYPE_ARCHER 100L
 /*
  * Class:     com_android_tv_tuner_TunerHal
  * Method:    nativeFinalize
  * Signature: (J)V
  */
-JNIEXPORT void JNICALL
-Java_com_android_tv_tuner_TunerHal_nativeFinalize(JNIEnv *, jobject, jlong);
+JNIEXPORT void JNICALL Java_com_android_tv_tuner_TunerHal_nativeFinalize
+  (JNIEnv *, jobject, jlong);
 
 /*
  * Class:     com_android_tv_tuner_TunerHal
  * Method:    nativeTune
  * Signature: (JILjava/lang/String;I)Z
  */
-JNIEXPORT jboolean JNICALL Java_com_android_tv_tuner_TunerHal_nativeTune(
-    JNIEnv *, jobject, jlong, jint, jstring, jint);
+JNIEXPORT jboolean JNICALL Java_com_android_tv_tuner_TunerHal_nativeTune
+  (JNIEnv *, jobject, jlong, jint, jstring, jint);
 
 /*
  * Class:     com_android_tv_tuner_TunerHal
  * Method:    nativeAddPidFilter
  * Signature: (JII)V
  */
-JNIEXPORT void JNICALL Java_com_android_tv_tuner_TunerHal_nativeAddPidFilter(
-    JNIEnv *, jobject, jlong, jint, jint);
+JNIEXPORT void JNICALL Java_com_android_tv_tuner_TunerHal_nativeAddPidFilter
+  (JNIEnv *, jobject, jlong, jint, jint);
 
 /*
  * Class:     com_android_tv_tuner_TunerHal
@@ -69,24 +83,8 @@
  * Signature: (J)V
  */
 JNIEXPORT void JNICALL
-Java_com_android_tv_tuner_TunerHal_nativeCloseAllPidFilters(JNIEnv *, jobject,
-                                                            jlong);
-
-/*
- * Class:     com_android_tv_tuner_TunerHal
- * Method:    nativeStopTune
- * Signature: (J)V
- */
-JNIEXPORT void JNICALL
-Java_com_android_tv_tuner_TunerHal_nativeStopTune(JNIEnv *, jobject, jlong);
-
-/*
- * Class:     com_android_tv_tuner_TunerHal
- * Method:    nativeWriteInBuffer
- * Signature: (J[BI)I
- */
-JNIEXPORT jint JNICALL Java_com_android_tv_tuner_TunerHal_nativeWriteInBuffer(
-    JNIEnv *, jobject, jlong, jbyteArray, jint);
+Java_com_android_tv_tuner_TunerHal_nativeCloseAllPidFilters
+  (JNIEnv *, jobject, jlong);
 
 /*
  * Class:     com_android_tv_tuner_TunerHal
@@ -94,29 +92,43 @@
  * Signature: (JZ)V
  */
 JNIEXPORT void JNICALL
-Java_com_android_tv_tuner_TunerHal_nativeSetHasPendingTune(JNIEnv *, jobject,
-                                                           jlong, jboolean);
+Java_com_android_tv_tuner_TunerHal_nativeSetHasPendingTune
+  (JNIEnv *, jobject, jlong, jboolean);
 
 /*
  * Class:     com_android_tv_tuner_TunerHal
  * Method:    nativeGetDeliverySystemType
  * Signature: (J)I
  */
-JNIEXPORT int JNICALL
-Java_com_android_tv_tuner_TunerHal_nativeGetDeliverySystemType(JNIEnv *,
-                                                               jobject, jlong);
+JNIEXPORT jint JNICALL
+Java_com_android_tv_tuner_TunerHal_nativeGetDeliverySystemType
+  (JNIEnv *, jobject, jlong);
 
-#ifdef __cplusplus
-}
-#endif
-#endif
-/* Header for class com_android_tv_tuner_TunerHal_FilterType */
+/*
+ * Class:     com_android_tv_tuner_TunerHal
+ * Method:    nativeGetSignalStrength
+ * Signature: (J)I
+ */
+JNIEXPORT jint JNICALL
+Java_com_android_tv_tuner_TunerHal_nativeGetSignalStrength
+  (JNIEnv *, jobject, jlong);
 
-#ifndef _Included_com_android_tv_tuner_TunerHal_FilterType
-#define _Included_com_android_tv_tuner_TunerHal_FilterType
-#ifdef __cplusplus
-extern "C" {
-#endif
+/*
+ * Class:     com_android_tv_tuner_TunerHal
+ * Method:    nativeStopTune
+ * Signature: (J)V
+ */
+JNIEXPORT void JNICALL Java_com_android_tv_tuner_TunerHal_nativeStopTune
+  (JNIEnv *, jobject, jlong);
+
+/*
+ * Class:     com_android_tv_tuner_TunerHal
+ * Method:    nativeWriteInBuffer
+ * Signature: (J[BI)I
+ */
+JNIEXPORT jint JNICALL Java_com_android_tv_tuner_TunerHal_nativeWriteInBuffer
+  (JNIEnv *, jobject, jlong, jbyteArray, jint);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/libs/Android.bp b/libs/Android.bp
new file mode 100644
index 0000000..fea9487
--- /dev/null
+++ b/libs/Android.bp
@@ -0,0 +1,165 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+java_import {
+    name: "tv-auto-factory-jar",
+    jars: ["auto-factory-1.0-beta2.jar"],
+    host_supported: true,
+    sdk_version: "current",
+}
+
+java_plugin {
+    name: "tv-auto-factory",
+    static_libs: [
+	"jsr330",
+        "tv-auto-factory-jar",
+        "tv-guava-jre-jar",
+	"tv-javawriter-jar",
+	"tv-javax-annotations-jar",
+    ],
+    processor_class: "com.google.auto.factory.processor.AutoFactoryProcessor",
+    generates_api: true,
+}
+
+
+java_import {
+    name: "tv-auto-value-jar",
+    jars: ["auto-value-1.5.2.jar"],
+    host_supported: true,
+    sdk_version: "current",
+}
+
+java_plugin {
+    name: "tv-auto-value",
+    static_libs: [
+        "tv-auto-value-jar",
+        "tv-guava-jre-jar",
+    ],
+    processor_class: "com.google.auto.value.processor.AutoValueProcessor",
+}
+
+java_import {
+    name: "tv-error-prone-annotations-jar",
+    jars: ["error_prone_annotations-2.3.1.jar"],
+    sdk_version: "current",
+}
+
+java_import {
+    name: "tv-guava-jre-jar",
+    jars: ["guava-23.3-jre.jar"],
+    host_supported: true,
+    sdk_version: "current",
+}
+
+java_import {
+    name: "tv-guava-android-jar",
+    jars: ["guava-23.6-android.jar"],
+    sdk_version: "current",
+}
+
+java_import_host{
+    name: "tv-javawriter-jar",
+    jars: ["javawriter-2.5.1.jar"],
+}
+
+java_import {
+    name: "tv-javax-annotations-jar",
+    jars: ["javax.annotation-api-1.2.jar"],
+    host_supported: true,
+    sdk_version: "current",
+}
+
+
+android_library_import {
+    name: "tv-lib-exoplayer",
+    aars: ["exoplayer-r1.5.16.aar"],
+    sdk_version: "current",
+}
+
+android_library_import {
+    name: "tv-lib-exoplayer-v2-core",
+    aars: ["exoplayer-core-2.9.0.aar"],
+    sdk_version: "current",
+}
+
+java_import_host {
+    name: "tv-lib-dagger-compiler-deps",
+    jars: [
+        "google-java-format-1.4-all-deps.jar",
+        "guava-23.3-jre.jar",
+        "javapoet-1.8.0.jar",
+    ],
+}
+
+java_import_host {
+    name: "tv-lib-dagger-compiler-import",
+    jars: [
+        "dagger-compiler-2.15.jar",
+        "dagger-producers-2.15.jar",
+        "dagger-spi-2.15.jar",
+    ],
+}
+
+java_import {
+    name: "tv-lib-dagger",
+    jars: ["dagger-2.15.jar"],
+    host_supported: true,
+    sdk_version: "current",
+}
+
+java_plugin {
+    name: "tv-lib-dagger-compiler",
+    static_libs: [
+        "tv-lib-dagger-compiler-import",
+        "tv-lib-dagger-compiler-deps",
+        "jsr330",
+        "tv-lib-dagger",
+    ],
+    processor_class: "dagger.internal.codegen.ComponentProcessor",
+    generates_api: true,
+}
+
+android_library_import {
+    name: "tv-lib-dagger-android",
+    aars: ["dagger-android-2.15.aar"],
+    sdk_version: "current",
+}
+
+java_import_host {
+    name: "tv-lib-dagger-android-processor-import",
+    jars: [
+        "dagger-android-jarimpl-2.15.jar",
+        "dagger-android-processor-2.15.jar",
+        "dagger-android-support-jarimpl-2.15.jar",
+    ],
+}
+
+java_plugin {
+    name: "tv-lib-dagger-android-processor",
+    static_libs: [
+        "tv-lib-dagger-android-processor-import",
+        "tv-lib-dagger-compiler-deps",
+        "jsr330",
+        "tv-lib-dagger",
+    ],
+    processor_class: "dagger.android.processor.AndroidProcessor",
+    generates_api: true,
+}
+
+java_import {
+    name: "tv-lib-truth",
+    jars: ["truth-0.36.jar"],
+    host_supported: true,
+    sdk_version: "current",
+}
diff --git a/libs/auto-factory-1.0-beta2.jar b/libs/auto-factory-1.0-beta2.jar
new file mode 100644
index 0000000..ceaddac
--- /dev/null
+++ b/libs/auto-factory-1.0-beta2.jar
Binary files differ
diff --git a/libs/auto-value-1.5.2.jar b/libs/auto-value-1.5.2.jar
new file mode 100644
index 0000000..8ac0567
--- /dev/null
+++ b/libs/auto-value-1.5.2.jar
Binary files differ
diff --git a/libs/dagger-2.15.jar b/libs/dagger-2.15.jar
new file mode 100644
index 0000000..6d76688
--- /dev/null
+++ b/libs/dagger-2.15.jar
Binary files differ
diff --git a/libs/dagger-android-2.15.aar b/libs/dagger-android-2.15.aar
new file mode 100644
index 0000000..430294a
--- /dev/null
+++ b/libs/dagger-android-2.15.aar
Binary files differ
diff --git a/libs/dagger-android-jarimpl-2.15.jar b/libs/dagger-android-jarimpl-2.15.jar
new file mode 100644
index 0000000..7f7cd45
--- /dev/null
+++ b/libs/dagger-android-jarimpl-2.15.jar
Binary files differ
diff --git a/libs/dagger-android-processor-2.15.jar b/libs/dagger-android-processor-2.15.jar
new file mode 100644
index 0000000..3c7ac05
--- /dev/null
+++ b/libs/dagger-android-processor-2.15.jar
Binary files differ
diff --git a/libs/dagger-android-support-2.15.aar b/libs/dagger-android-support-2.15.aar
new file mode 100644
index 0000000..89a71a9
--- /dev/null
+++ b/libs/dagger-android-support-2.15.aar
Binary files differ
diff --git a/libs/dagger-android-support-jarimpl-2.15.jar b/libs/dagger-android-support-jarimpl-2.15.jar
new file mode 100644
index 0000000..d0ea01a
--- /dev/null
+++ b/libs/dagger-android-support-jarimpl-2.15.jar
Binary files differ
diff --git a/libs/dagger-compiler-2.15.jar b/libs/dagger-compiler-2.15.jar
new file mode 100644
index 0000000..e73110f
--- /dev/null
+++ b/libs/dagger-compiler-2.15.jar
Binary files differ
diff --git a/libs/dagger-producers-2.15.jar b/libs/dagger-producers-2.15.jar
new file mode 100644
index 0000000..f1dbb07
--- /dev/null
+++ b/libs/dagger-producers-2.15.jar
Binary files differ
diff --git a/libs/dagger-spi-2.15.jar b/libs/dagger-spi-2.15.jar
new file mode 100644
index 0000000..6e3156a
--- /dev/null
+++ b/libs/dagger-spi-2.15.jar
Binary files differ
diff --git a/libs/error_prone_annotations-2.3.1.jar b/libs/error_prone_annotations-2.3.1.jar
new file mode 100644
index 0000000..8a0efa3
--- /dev/null
+++ b/libs/error_prone_annotations-2.3.1.jar
Binary files differ
diff --git a/libs/exoplayer-core-2-SNAPHOT-20180114.aar b/libs/exoplayer-core-2-SNAPHOT-20180114.aar
deleted file mode 100644
index 90af2e6..0000000
--- a/libs/exoplayer-core-2-SNAPHOT-20180114.aar
+++ /dev/null
Binary files differ
diff --git a/libs/exoplayer-core-2.9.0.aar b/libs/exoplayer-core-2.9.0.aar
new file mode 100644
index 0000000..64c4f37
--- /dev/null
+++ b/libs/exoplayer-core-2.9.0.aar
Binary files differ
diff --git a/libs/google-java-format-1.4-all-deps.jar b/libs/google-java-format-1.4-all-deps.jar
new file mode 100644
index 0000000..b10bfbd
--- /dev/null
+++ b/libs/google-java-format-1.4-all-deps.jar
Binary files differ
diff --git a/libs/guava-23.3-jre.jar b/libs/guava-23.3-jre.jar
new file mode 100644
index 0000000..b13e275
--- /dev/null
+++ b/libs/guava-23.3-jre.jar
Binary files differ
diff --git a/libs/guava-23.5-jre.jar b/libs/guava-23.5-jre.jar
new file mode 100644
index 0000000..7e5f13a
--- /dev/null
+++ b/libs/guava-23.5-jre.jar
Binary files differ
diff --git a/libs/guava-23.6-android.jar b/libs/guava-23.6-android.jar
new file mode 100644
index 0000000..01180d2
--- /dev/null
+++ b/libs/guava-23.6-android.jar
Binary files differ
diff --git a/libs/javapoet-1.8.0.jar b/libs/javapoet-1.8.0.jar
new file mode 100644
index 0000000..6758b6d
--- /dev/null
+++ b/libs/javapoet-1.8.0.jar
Binary files differ
diff --git a/libs/javawriter-2.5.1.jar b/libs/javawriter-2.5.1.jar
new file mode 100644
index 0000000..4ec579e
--- /dev/null
+++ b/libs/javawriter-2.5.1.jar
Binary files differ
diff --git a/libs/javax.annotation-api-1.2.jar b/libs/javax.annotation-api-1.2.jar
new file mode 100644
index 0000000..9ab39ff
--- /dev/null
+++ b/libs/javax.annotation-api-1.2.jar
Binary files differ
diff --git a/libs/truth-0.36.jar b/libs/truth-0.36.jar
new file mode 100644
index 0000000..8174e4a
--- /dev/null
+++ b/libs/truth-0.36.jar
Binary files differ
diff --git a/material_res/drawable-hdpi/quantum_ic_arrow_downward_white_36.png b/material_res/drawable-hdpi/quantum_ic_arrow_downward_white_36.png
new file mode 100644
index 0000000..76a57d1
--- /dev/null
+++ b/material_res/drawable-hdpi/quantum_ic_arrow_downward_white_36.png
Binary files differ
diff --git a/material_res/drawable-hdpi/quantum_ic_arrow_upward_white_36.png b/material_res/drawable-hdpi/quantum_ic_arrow_upward_white_36.png
new file mode 100644
index 0000000..ce42f36
--- /dev/null
+++ b/material_res/drawable-hdpi/quantum_ic_arrow_upward_white_36.png
Binary files differ
diff --git a/material_res/drawable-hdpi/quantum_ic_check_circle_white_48.png b/material_res/drawable-hdpi/quantum_ic_check_circle_white_48.png
new file mode 100644
index 0000000..4f96745
--- /dev/null
+++ b/material_res/drawable-hdpi/quantum_ic_check_circle_white_48.png
Binary files differ
diff --git a/material_res/drawable-hdpi/quantum_ic_developer_mode_tv_white_48.png b/material_res/drawable-hdpi/quantum_ic_developer_mode_tv_white_48.png
new file mode 100644
index 0000000..0d9bf8b
--- /dev/null
+++ b/material_res/drawable-hdpi/quantum_ic_developer_mode_tv_white_48.png
Binary files differ
diff --git a/material_res/drawable-hdpi/quantum_ic_error_white_48.png b/material_res/drawable-hdpi/quantum_ic_error_white_48.png
new file mode 100644
index 0000000..abe2573
--- /dev/null
+++ b/material_res/drawable-hdpi/quantum_ic_error_white_48.png
Binary files differ
diff --git a/material_res/drawable-hdpi/quantum_ic_signal_cellular_0_bar_white_24.png b/material_res/drawable-hdpi/quantum_ic_signal_cellular_0_bar_white_24.png
new file mode 100644
index 0000000..c7e8848
--- /dev/null
+++ b/material_res/drawable-hdpi/quantum_ic_signal_cellular_0_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-hdpi/quantum_ic_signal_cellular_1_bar_white_24.png b/material_res/drawable-hdpi/quantum_ic_signal_cellular_1_bar_white_24.png
new file mode 100644
index 0000000..3b79370
--- /dev/null
+++ b/material_res/drawable-hdpi/quantum_ic_signal_cellular_1_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-hdpi/quantum_ic_signal_cellular_2_bar_white_24.png b/material_res/drawable-hdpi/quantum_ic_signal_cellular_2_bar_white_24.png
new file mode 100644
index 0000000..bf736dc
--- /dev/null
+++ b/material_res/drawable-hdpi/quantum_ic_signal_cellular_2_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-hdpi/quantum_ic_signal_cellular_3_bar_white_24.png b/material_res/drawable-hdpi/quantum_ic_signal_cellular_3_bar_white_24.png
new file mode 100644
index 0000000..8bee424
--- /dev/null
+++ b/material_res/drawable-hdpi/quantum_ic_signal_cellular_3_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-hdpi/quantum_ic_signal_cellular_4_bar_white_24.png b/material_res/drawable-hdpi/quantum_ic_signal_cellular_4_bar_white_24.png
new file mode 100644
index 0000000..1765e94
--- /dev/null
+++ b/material_res/drawable-hdpi/quantum_ic_signal_cellular_4_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-hdpi/quantum_ic_warning_white_18.png b/material_res/drawable-hdpi/quantum_ic_warning_white_18.png
new file mode 100644
index 0000000..7520b79
--- /dev/null
+++ b/material_res/drawable-hdpi/quantum_ic_warning_white_18.png
Binary files differ
diff --git a/material_res/drawable-hdpi/quantum_ic_warning_white_96.png b/material_res/drawable-hdpi/quantum_ic_warning_white_96.png
new file mode 100644
index 0000000..88c2232
--- /dev/null
+++ b/material_res/drawable-hdpi/quantum_ic_warning_white_96.png
Binary files differ
diff --git a/material_res/drawable-mdpi/quantum_ic_arrow_downward_white_36.png b/material_res/drawable-mdpi/quantum_ic_arrow_downward_white_36.png
new file mode 100644
index 0000000..dbfb7ab
--- /dev/null
+++ b/material_res/drawable-mdpi/quantum_ic_arrow_downward_white_36.png
Binary files differ
diff --git a/material_res/drawable-mdpi/quantum_ic_arrow_upward_white_36.png b/material_res/drawable-mdpi/quantum_ic_arrow_upward_white_36.png
new file mode 100644
index 0000000..c39725c
--- /dev/null
+++ b/material_res/drawable-mdpi/quantum_ic_arrow_upward_white_36.png
Binary files differ
diff --git a/material_res/drawable-mdpi/quantum_ic_check_circle_white_48.png b/material_res/drawable-mdpi/quantum_ic_check_circle_white_48.png
new file mode 100644
index 0000000..d36d696
--- /dev/null
+++ b/material_res/drawable-mdpi/quantum_ic_check_circle_white_48.png
Binary files differ
diff --git a/material_res/drawable-mdpi/quantum_ic_developer_mode_tv_white_48.png b/material_res/drawable-mdpi/quantum_ic_developer_mode_tv_white_48.png
new file mode 100644
index 0000000..976a44b
--- /dev/null
+++ b/material_res/drawable-mdpi/quantum_ic_developer_mode_tv_white_48.png
Binary files differ
diff --git a/material_res/drawable-mdpi/quantum_ic_error_white_48.png b/material_res/drawable-mdpi/quantum_ic_error_white_48.png
new file mode 100644
index 0000000..9829698
--- /dev/null
+++ b/material_res/drawable-mdpi/quantum_ic_error_white_48.png
Binary files differ
diff --git a/material_res/drawable-mdpi/quantum_ic_signal_cellular_0_bar_white_24.png b/material_res/drawable-mdpi/quantum_ic_signal_cellular_0_bar_white_24.png
new file mode 100644
index 0000000..a0c5d41
--- /dev/null
+++ b/material_res/drawable-mdpi/quantum_ic_signal_cellular_0_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-mdpi/quantum_ic_signal_cellular_1_bar_white_24.png b/material_res/drawable-mdpi/quantum_ic_signal_cellular_1_bar_white_24.png
new file mode 100644
index 0000000..0e70e98
--- /dev/null
+++ b/material_res/drawable-mdpi/quantum_ic_signal_cellular_1_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-mdpi/quantum_ic_signal_cellular_2_bar_white_24.png b/material_res/drawable-mdpi/quantum_ic_signal_cellular_2_bar_white_24.png
new file mode 100644
index 0000000..eb2124d
--- /dev/null
+++ b/material_res/drawable-mdpi/quantum_ic_signal_cellular_2_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-mdpi/quantum_ic_signal_cellular_3_bar_white_24.png b/material_res/drawable-mdpi/quantum_ic_signal_cellular_3_bar_white_24.png
new file mode 100644
index 0000000..a2045c3
--- /dev/null
+++ b/material_res/drawable-mdpi/quantum_ic_signal_cellular_3_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-mdpi/quantum_ic_signal_cellular_4_bar_white_24.png b/material_res/drawable-mdpi/quantum_ic_signal_cellular_4_bar_white_24.png
new file mode 100644
index 0000000..8d76fd4
--- /dev/null
+++ b/material_res/drawable-mdpi/quantum_ic_signal_cellular_4_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-mdpi/quantum_ic_warning_white_18.png b/material_res/drawable-mdpi/quantum_ic_warning_white_18.png
new file mode 100644
index 0000000..1f05517
--- /dev/null
+++ b/material_res/drawable-mdpi/quantum_ic_warning_white_18.png
Binary files differ
diff --git a/material_res/drawable-mdpi/quantum_ic_warning_white_96.png b/material_res/drawable-mdpi/quantum_ic_warning_white_96.png
new file mode 100644
index 0000000..8683a2e
--- /dev/null
+++ b/material_res/drawable-mdpi/quantum_ic_warning_white_96.png
Binary files differ
diff --git a/material_res/drawable-xhdpi/quantum_ic_arrow_downward_white_36.png b/material_res/drawable-xhdpi/quantum_ic_arrow_downward_white_36.png
new file mode 100644
index 0000000..225e4e5
--- /dev/null
+++ b/material_res/drawable-xhdpi/quantum_ic_arrow_downward_white_36.png
Binary files differ
diff --git a/material_res/drawable-xhdpi/quantum_ic_arrow_upward_white_36.png b/material_res/drawable-xhdpi/quantum_ic_arrow_upward_white_36.png
new file mode 100644
index 0000000..d7b27da
--- /dev/null
+++ b/material_res/drawable-xhdpi/quantum_ic_arrow_upward_white_36.png
Binary files differ
diff --git a/material_res/drawable-xhdpi/quantum_ic_check_circle_white_48.png b/material_res/drawable-xhdpi/quantum_ic_check_circle_white_48.png
new file mode 100644
index 0000000..2c6e474
--- /dev/null
+++ b/material_res/drawable-xhdpi/quantum_ic_check_circle_white_48.png
Binary files differ
diff --git a/material_res/drawable-xhdpi/quantum_ic_developer_mode_tv_white_48.png b/material_res/drawable-xhdpi/quantum_ic_developer_mode_tv_white_48.png
new file mode 100644
index 0000000..1336147
--- /dev/null
+++ b/material_res/drawable-xhdpi/quantum_ic_developer_mode_tv_white_48.png
Binary files differ
diff --git a/material_res/drawable-xhdpi/quantum_ic_error_white_48.png b/material_res/drawable-xhdpi/quantum_ic_error_white_48.png
new file mode 100644
index 0000000..830fb7e
--- /dev/null
+++ b/material_res/drawable-xhdpi/quantum_ic_error_white_48.png
Binary files differ
diff --git a/material_res/drawable-xhdpi/quantum_ic_signal_cellular_0_bar_white_24.png b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_0_bar_white_24.png
new file mode 100644
index 0000000..26d6bff
--- /dev/null
+++ b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_0_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xhdpi/quantum_ic_signal_cellular_1_bar_white_24.png b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_1_bar_white_24.png
new file mode 100644
index 0000000..3b7f7ad
--- /dev/null
+++ b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_1_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xhdpi/quantum_ic_signal_cellular_2_bar_white_24.png b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_2_bar_white_24.png
new file mode 100644
index 0000000..b80307c
--- /dev/null
+++ b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_2_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xhdpi/quantum_ic_signal_cellular_3_bar_white_24.png b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_3_bar_white_24.png
new file mode 100644
index 0000000..1ccc977
--- /dev/null
+++ b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_3_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xhdpi/quantum_ic_signal_cellular_4_bar_white_24.png b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_4_bar_white_24.png
new file mode 100644
index 0000000..62501b0
--- /dev/null
+++ b/material_res/drawable-xhdpi/quantum_ic_signal_cellular_4_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xhdpi/quantum_ic_warning_white_18.png b/material_res/drawable-xhdpi/quantum_ic_warning_white_18.png
new file mode 100644
index 0000000..55c6843
--- /dev/null
+++ b/material_res/drawable-xhdpi/quantum_ic_warning_white_18.png
Binary files differ
diff --git a/material_res/drawable-xhdpi/quantum_ic_warning_white_96.png b/material_res/drawable-xhdpi/quantum_ic_warning_white_96.png
new file mode 100644
index 0000000..23e6d93
--- /dev/null
+++ b/material_res/drawable-xhdpi/quantum_ic_warning_white_96.png
Binary files differ
diff --git a/material_res/drawable-xxhdpi/quantum_ic_arrow_downward_white_36.png b/material_res/drawable-xxhdpi/quantum_ic_arrow_downward_white_36.png
new file mode 100644
index 0000000..0502223
--- /dev/null
+++ b/material_res/drawable-xxhdpi/quantum_ic_arrow_downward_white_36.png
Binary files differ
diff --git a/material_res/drawable-xxhdpi/quantum_ic_arrow_upward_white_36.png b/material_res/drawable-xxhdpi/quantum_ic_arrow_upward_white_36.png
new file mode 100644
index 0000000..eceb34c
--- /dev/null
+++ b/material_res/drawable-xxhdpi/quantum_ic_arrow_upward_white_36.png
Binary files differ
diff --git a/material_res/drawable-xxhdpi/quantum_ic_check_circle_white_48.png b/material_res/drawable-xxhdpi/quantum_ic_check_circle_white_48.png
new file mode 100644
index 0000000..980d10b
--- /dev/null
+++ b/material_res/drawable-xxhdpi/quantum_ic_check_circle_white_48.png
Binary files differ
diff --git a/material_res/drawable-xxhdpi/quantum_ic_developer_mode_tv_white_48.png b/material_res/drawable-xxhdpi/quantum_ic_developer_mode_tv_white_48.png
new file mode 100644
index 0000000..defa1bf
--- /dev/null
+++ b/material_res/drawable-xxhdpi/quantum_ic_developer_mode_tv_white_48.png
Binary files differ
diff --git a/material_res/drawable-xxhdpi/quantum_ic_error_white_48.png b/material_res/drawable-xxhdpi/quantum_ic_error_white_48.png
new file mode 100644
index 0000000..c834952
--- /dev/null
+++ b/material_res/drawable-xxhdpi/quantum_ic_error_white_48.png
Binary files differ
diff --git a/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_0_bar_white_24.png b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_0_bar_white_24.png
new file mode 100644
index 0000000..2e200fb
--- /dev/null
+++ b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_0_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_1_bar_white_24.png b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_1_bar_white_24.png
new file mode 100644
index 0000000..215f9b7
--- /dev/null
+++ b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_1_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_2_bar_white_24.png b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_2_bar_white_24.png
new file mode 100644
index 0000000..c8c0ebf
--- /dev/null
+++ b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_2_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_3_bar_white_24.png b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_3_bar_white_24.png
new file mode 100644
index 0000000..8377811
--- /dev/null
+++ b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_3_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_4_bar_white_24.png b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_4_bar_white_24.png
new file mode 100644
index 0000000..b191b9d
--- /dev/null
+++ b/material_res/drawable-xxhdpi/quantum_ic_signal_cellular_4_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xxhdpi/quantum_ic_warning_white_18.png b/material_res/drawable-xxhdpi/quantum_ic_warning_white_18.png
new file mode 100644
index 0000000..fb079e9
--- /dev/null
+++ b/material_res/drawable-xxhdpi/quantum_ic_warning_white_18.png
Binary files differ
diff --git a/material_res/drawable-xxhdpi/quantum_ic_warning_white_96.png b/material_res/drawable-xxhdpi/quantum_ic_warning_white_96.png
new file mode 100644
index 0000000..064cd51
--- /dev/null
+++ b/material_res/drawable-xxhdpi/quantum_ic_warning_white_96.png
Binary files differ
diff --git a/material_res/drawable-xxxhdpi/quantum_ic_arrow_downward_white_36.png b/material_res/drawable-xxxhdpi/quantum_ic_arrow_downward_white_36.png
new file mode 100644
index 0000000..fe0ecc6
--- /dev/null
+++ b/material_res/drawable-xxxhdpi/quantum_ic_arrow_downward_white_36.png
Binary files differ
diff --git a/material_res/drawable-xxxhdpi/quantum_ic_arrow_upward_white_36.png b/material_res/drawable-xxxhdpi/quantum_ic_arrow_upward_white_36.png
new file mode 100644
index 0000000..5e61c3d
--- /dev/null
+++ b/material_res/drawable-xxxhdpi/quantum_ic_arrow_upward_white_36.png
Binary files differ
diff --git a/material_res/drawable-xxxhdpi/quantum_ic_check_circle_white_48.png b/material_res/drawable-xxxhdpi/quantum_ic_check_circle_white_48.png
new file mode 100644
index 0000000..60463c5
--- /dev/null
+++ b/material_res/drawable-xxxhdpi/quantum_ic_check_circle_white_48.png
Binary files differ
diff --git a/material_res/drawable-xxxhdpi/quantum_ic_developer_mode_tv_white_48.png b/material_res/drawable-xxxhdpi/quantum_ic_developer_mode_tv_white_48.png
new file mode 100644
index 0000000..b316f1d
--- /dev/null
+++ b/material_res/drawable-xxxhdpi/quantum_ic_developer_mode_tv_white_48.png
Binary files differ
diff --git a/material_res/drawable-xxxhdpi/quantum_ic_error_white_48.png b/material_res/drawable-xxxhdpi/quantum_ic_error_white_48.png
new file mode 100644
index 0000000..ad4a474
--- /dev/null
+++ b/material_res/drawable-xxxhdpi/quantum_ic_error_white_48.png
Binary files differ
diff --git a/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_0_bar_white_24.png b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_0_bar_white_24.png
new file mode 100644
index 0000000..f805132
--- /dev/null
+++ b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_0_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_1_bar_white_24.png b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_1_bar_white_24.png
new file mode 100644
index 0000000..42debcc
--- /dev/null
+++ b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_1_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_2_bar_white_24.png b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_2_bar_white_24.png
new file mode 100644
index 0000000..6a7f320
--- /dev/null
+++ b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_2_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_3_bar_white_24.png b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_3_bar_white_24.png
new file mode 100644
index 0000000..706da32
--- /dev/null
+++ b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_3_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_4_bar_white_24.png b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_4_bar_white_24.png
new file mode 100644
index 0000000..4a3ac28
--- /dev/null
+++ b/material_res/drawable-xxxhdpi/quantum_ic_signal_cellular_4_bar_white_24.png
Binary files differ
diff --git a/material_res/drawable-xxxhdpi/quantum_ic_warning_white_18.png b/material_res/drawable-xxxhdpi/quantum_ic_warning_white_18.png
new file mode 100644
index 0000000..807b9fa
--- /dev/null
+++ b/material_res/drawable-xxxhdpi/quantum_ic_warning_white_18.png
Binary files differ
diff --git a/material_res/drawable-xxxhdpi/quantum_ic_warning_white_96.png b/material_res/drawable-xxxhdpi/quantum_ic_warning_white_96.png
new file mode 100644
index 0000000..2439be1
--- /dev/null
+++ b/material_res/drawable-xxxhdpi/quantum_ic_warning_white_96.png
Binary files differ
diff --git a/open_source_project.README b/open_source_project.README
index 31532f0..897b8ee 100644
--- a/open_source_project.README
+++ b/open_source_project.README
@@ -7,7 +7,7 @@
    https://source.android.com/source/building.html
    (Developers using PDK can skip the step 1.)
 2. Enable the feature PackageManager.FEATURE_LIVE_TV.
-3. Put this project under Android platform repository.
+3. Put this project under Android platform repository if required.
 4. Include this package inside platform build.
 5. Build the platform.
    https://source.android.com/source/building.html
diff --git a/partner_support/Android.bp b/partner_support/Android.bp
new file mode 100644
index 0000000..4775fc1
--- /dev/null
+++ b/partner_support/Android.bp
@@ -0,0 +1,32 @@
+//
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+android_library {
+    name: "live-channels-partner-support",
+    srcs: ["src/**/*.java"],
+
+    sdk_version: "system_current",
+    min_sdk_version: "23",
+
+    resource_dirs: ["res"],
+
+    static_libs: ["android-support-annotations"],
+
+    libs: ["tv-auto-value-jar"],
+
+    plugins: ["tv-auto-value"],
+
+}
diff --git a/partner_support/Android.mk b/partner_support/Android.mk
deleted file mode 100644
index 8306921..0000000
--- a/partner_support/Android.mk
+++ /dev/null
@@ -1,23 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-# Include all java files.
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_MODULE := live-channels-partner-support
-LOCAL_MODULE_CLASS := STATIC_JAVA_LIBRARIES
-LOCAL_MODULE_TAGS := optional
-LOCAL_SDK_VERSION := system_current
-LOCAL_MIN_SDK_VERSION := 23
-
-LOCAL_USE_AAPT2 := true
-
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-
-LOCAL_STATIC_JAVA_LIBRARIES := android-support-annotations
-
-include $(LOCAL_PATH)/buildconfig.mk
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
-
-include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/partner_support/AndroidManifest.xml b/partner_support/AndroidManifest.xml
index 5a45f0d..45a693f 100644
--- a/partner_support/AndroidManifest.xml
+++ b/partner_support/AndroidManifest.xml
@@ -17,6 +17,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.google.android.tv.partner.support"
     android:versionCode="1">
-  <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="21"/>
+  <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
   <application />
 </manifest>
diff --git a/partner_support/g3doc/SeriesIdColumnForPartners.md b/partner_support/g3doc/SeriesIdColumnForPartners.md
new file mode 100644
index 0000000..cd44db0
--- /dev/null
+++ b/partner_support/g3doc/SeriesIdColumnForPartners.md
@@ -0,0 +1,30 @@
+# 3rd party instructions for using series recording feature of Live Channels
+
+## Prerequisites
+
+*   Updated agreement with Google
+*   Oreo or patched Nougat
+
+## Nougat
+
+To enable series recording with Nougat you will need the following changes.
+
+### Patch TVProvider
+
+To run in Nougat you must backport the following changes
+
+*   [Filter out non-existing customized columns in
+    DB](https://partner-android.googlesource.com/platform/packages/providers/TvProvider/+/142162af889b2c124bb012eea608c6a65eed54bb)
+*   [Add TvProvider methods to get and add
+    columns](https://partner-android.googlesource.com/platform/packages/providers/TvProvider/+/cda6788ae903513a555fd3e07a5a1c14218c40a2)
+
+### Customisation
+
+Indicate TvProvider is patched by including the following in their TV
+customization resource
+
+```
+<bool name="tvprovider_allows_column_creation">true</bool>
+```
+
+See https://source.android.com/devices/tv/customize-tv-app
diff --git a/partner_support/g3doc/TurnOffEmbeddedTuner.md b/partner_support/g3doc/TurnOffEmbeddedTuner.md
new file mode 100644
index 0000000..0ba7cff
--- /dev/null
+++ b/partner_support/g3doc/TurnOffEmbeddedTuner.md
@@ -0,0 +1,15 @@
+# 3rd party instructions turning off the embedded tuner in Live Channels
+
+Partners that have a built in tuner should provide a TV Input like
+SampleDvbTuner. When partners provide their own tuner they MUST turn of the
+embedded tuner in Live Channels.
+
+### Customisation
+
+Indicate Live Channels should not use it's embedded tuner implementation.
+
+```
+<bool name="turn_off_embedded_tuner">true</bool>
+```
+
+See https://source.android.com/devices/tv/customize-tv-app
diff --git a/partner_support/sample_customization/AndroidManifest.xml b/partner_support/sample_customization/AndroidManifest.xml
index f1edad3..804691a 100644
--- a/partner_support/sample_customization/AndroidManifest.xml
+++ b/partner_support/sample_customization/AndroidManifest.xml
@@ -18,13 +18,13 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
         package="com.example.tvcustomization">
     <!-- Customization package must have this permission to customize TV apps. -->
-    <uses-permission android:name="com.android.tv.permission.CUSTOMIZE_TV_APP"/>
+    <uses-permission android:name="com.google.android.tv.permission.CUSTOMIZE_TV_APP"/>
 
     <!-- Enable leanback library support. -->
     <uses-feature android:name="android.software.leanback" android:required="true" />
     <uses-feature android:name="android.hardware.touchscreen" android:required="false" />
 
-    <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="23"/>
+    <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
 
     <application android:label="Partner Customization"
             android:theme="@android:style/Theme.Holo.Light.NoActionBar"
diff --git a/partner_support/sample_customization/res/mipmap-hdpi/ic_launcher.png b/partner_support/sample_customization/res/mipmap-hdpi/ic_launcher.png
old mode 100755
new mode 100644
index 26add7f..454c515
--- a/partner_support/sample_customization/res/mipmap-hdpi/ic_launcher.png
+++ b/partner_support/sample_customization/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/partner_support/sample_customization/res/mipmap-mdpi/ic_launcher.png b/partner_support/sample_customization/res/mipmap-mdpi/ic_launcher.png
old mode 100755
new mode 100644
index 1ac20db..5e53eb5
--- a/partner_support/sample_customization/res/mipmap-mdpi/ic_launcher.png
+++ b/partner_support/sample_customization/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/partner_support/sample_customization/res/mipmap-xhdpi/ic_launcher.png b/partner_support/sample_customization/res/mipmap-xhdpi/ic_launcher.png
old mode 100755
new mode 100644
index f6cf645..898bac4
--- a/partner_support/sample_customization/res/mipmap-xhdpi/ic_launcher.png
+++ b/partner_support/sample_customization/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/partner_support/sample_customization/res/mipmap-xxhdpi/ic_launcher.png b/partner_support/sample_customization/res/mipmap-xxhdpi/ic_launcher.png
old mode 100755
new mode 100644
index 72a250d..9da2990
--- a/partner_support/sample_customization/res/mipmap-xxhdpi/ic_launcher.png
+++ b/partner_support/sample_customization/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/partner_support/sample_customization/res/mipmap-xxxhdpi/ic_launcher.png b/partner_support/sample_customization/res/mipmap-xxxhdpi/ic_launcher.png
old mode 100755
new mode 100644
index 648001f..ff5c4b1
--- a/partner_support/sample_customization/res/mipmap-xxxhdpi/ic_launcher.png
+++ b/partner_support/sample_customization/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/partner_support/sample_customization/res/values/bools.xml b/partner_support/sample_customization/res/values/bools.xml
index 54fbe07..259548b 100644
--- a/partner_support/sample_customization/res/values/bools.xml
+++ b/partner_support/sample_customization/res/values/bools.xml
@@ -17,4 +17,6 @@
 
 <resources>
     <bool name="tvprovider_allows_system_inserts_to_program_table">true</bool>
+    <bool name="tvprovider_allows_column_creation">true</bool>
+    <bool name="turn_off_embedded_tuner">true</bool>
 </resources>
\ No newline at end of file
diff --git a/partner_support/samples/Android.mk b/partner_support/samples/Android.mk
index 922a5b5..2e771a5 100644
--- a/partner_support/samples/Android.mk
+++ b/partner_support/samples/Android.mk
@@ -16,7 +16,7 @@
     android-support-core-ui \
     android-support-v7-recyclerview \
     android-support-v17-leanback \
-    android-support-tv-provider
+    androidx.tvprovider_tvprovider
 
 LOCAL_USE_AAPT2 := true
 
diff --git a/partner_support/samples/AndroidManifest.xml b/partner_support/samples/AndroidManifest.xml
index b9e086c..d91c603 100644
--- a/partner_support/samples/AndroidManifest.xml
+++ b/partner_support/samples/AndroidManifest.xml
@@ -29,12 +29,13 @@
     <uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" />
     <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
     <uses-permission android:name="com.android.tv.permission.RECEIVE_INPUT_EVENT" />
-    <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="23"/>
+    <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
     <!--TODO(b/68949299): remove tool hint when we have smaller dependency targets-->
     <application android:label="@string/partner_support_sample_tv_input"
-            tools:replace="android:label,icon,theme"
+            tools:replace="android:label,icon,theme,appComponentFactory"
             android:icon="@mipmap/ic_launcher"
-            android:theme="@android:style/Theme.Holo.Light.NoActionBar" >
+            android:theme="@android:style/Theme.Holo.Light.NoActionBar"
+            android:appComponentFactory="android.support.v4.app.CoreComponentFactory" >
         <activity android:name=".SampleTvInputSetupActivity"
                 android:theme="@style/Theme.Leanback.GuidedStep">
             <intent-filter>
diff --git a/partner_support/samples/res/mipmap-hdpi/ic_launcher.png b/partner_support/samples/res/mipmap-hdpi/ic_launcher.png
old mode 100755
new mode 100644
index a827add..a044d2c
--- a/partner_support/samples/res/mipmap-hdpi/ic_launcher.png
+++ b/partner_support/samples/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/partner_support/samples/res/mipmap-mdpi/ic_launcher.png b/partner_support/samples/res/mipmap-mdpi/ic_launcher.png
old mode 100755
new mode 100644
index d7d36f2..26307c2
--- a/partner_support/samples/res/mipmap-mdpi/ic_launcher.png
+++ b/partner_support/samples/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/partner_support/samples/res/mipmap-xhdpi/ic_launcher.png b/partner_support/samples/res/mipmap-xhdpi/ic_launcher.png
old mode 100755
new mode 100644
index 210bfcf..4964683
--- a/partner_support/samples/res/mipmap-xhdpi/ic_launcher.png
+++ b/partner_support/samples/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/partner_support/samples/res/mipmap-xxhdpi/ic_launcher.png b/partner_support/samples/res/mipmap-xxhdpi/ic_launcher.png
old mode 100755
new mode 100644
index 59a090c..93db554
--- a/partner_support/samples/res/mipmap-xxhdpi/ic_launcher.png
+++ b/partner_support/samples/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/partner_support/samples/res/mipmap-xxxhdpi/ic_launcher.png b/partner_support/samples/res/mipmap-xxxhdpi/ic_launcher.png
old mode 100755
new mode 100644
index 388b6eb..cfc2fb1
--- a/partner_support/samples/res/mipmap-xxxhdpi/ic_launcher.png
+++ b/partner_support/samples/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/partner_support/samples/src/com/example/partnersupportsampletvinput/ChannelScanFragment.java b/partner_support/samples/src/com/example/partnersupportsampletvinput/ChannelScanFragment.java
index 35f4b69..ec7589c 100644
--- a/partner_support/samples/src/com/example/partnersupportsampletvinput/ChannelScanFragment.java
+++ b/partner_support/samples/src/com/example/partnersupportsampletvinput/ChannelScanFragment.java
@@ -23,8 +23,6 @@
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.support.annotation.NonNull;
-import android.support.media.tv.Channel;
-import android.support.media.tv.TvContractCompat;
 import android.support.v17.leanback.app.GuidedStepFragment;
 import android.support.v17.leanback.widget.GuidanceStylist;
 import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
@@ -32,6 +30,8 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import androidx.tvprovider.media.tv.Channel;
+import androidx.tvprovider.media.tv.TvContractCompat;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
diff --git a/partner_support/src/com/google/android/tv/partner/support/AutoValue_EpgInput.java b/partner_support/src/com/google/android/tv/partner/support/AutoValue_EpgInput.java
deleted file mode 100644
index aad51c7..0000000
--- a/partner_support/src/com/google/android/tv/partner/support/AutoValue_EpgInput.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.google.android.tv.partner.support;
-
-
-
-/**
- * Hand copy of generated Autovalue class.
- *
- * TODO get autovalue working
- */
-final class AutoValue_EpgInput extends EpgInput {
-
-    private final long id;
-    private final String inputId;
-    private final String lineupId;
-
-    AutoValue_EpgInput(
-            long id,
-            String inputId,
-            String lineupId) {
-        this.id = id;
-        if (inputId == null) {
-            throw new NullPointerException("Null inputId");
-        }
-        this.inputId = inputId;
-        if (lineupId == null) {
-            throw new NullPointerException("Null lineupId");
-        }
-        this.lineupId = lineupId;
-    }
-
-    @Override
-    public long getId() {
-        return id;
-    }
-
-    @Override
-    public String getInputId() {
-        return inputId;
-    }
-
-    @Override
-    public String getLineupId() {
-        return lineupId;
-    }
-
-    @Override
-    public String toString() {
-        return "EpgInput{"
-                + "id=" + id + ", "
-                + "inputId=" + inputId + ", "
-                + "lineupId=" + lineupId
-                + "}";
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (o == this) {
-            return true;
-        }
-        if (o instanceof EpgInput) {
-            EpgInput that = (EpgInput) o;
-            return (this.id == that.getId())
-                    && (this.inputId.equals(that.getInputId()))
-                    && (this.lineupId.equals(that.getLineupId()));
-        }
-        return false;
-    }
-
-    @Override
-    public int hashCode() {
-        int h$ = 1;
-        h$ *= 1000003;
-        h$ ^= (int) ((id >>> 32) ^ id);
-        h$ *= 1000003;
-        h$ ^= inputId.hashCode();
-        h$ *= 1000003;
-        h$ ^= lineupId.hashCode();
-        return h$;
-    }
-
-}
diff --git a/partner_support/src/com/google/android/tv/partner/support/AutoValue_Lineup.java b/partner_support/src/com/google/android/tv/partner/support/AutoValue_Lineup.java
deleted file mode 100644
index 076f8a2..0000000
--- a/partner_support/src/com/google/android/tv/partner/support/AutoValue_Lineup.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-
-
-package com.google.android.tv.partner.support;
-
-import android.support.annotation.Nullable;
-import java.util.List;
-
-/**
- * Hand copy of generated Autovalue class.
- *
- * TODO get autovalue working
- */
-
-final class AutoValue_Lineup extends Lineup {
-
-    private final String id;
-    private final int type;
-    private final String name;
-    private final List<String> channels;
-
-    AutoValue_Lineup(
-            String id,
-            int type,
-            @Nullable String name,
-            List<String> channels) {
-        if (id == null) {
-            throw new NullPointerException("Null id");
-        }
-        this.id = id;
-        this.type = type;
-        this.name = name;
-        if (channels == null) {
-            throw new NullPointerException("Null channels");
-        }
-        this.channels = channels;
-    }
-
-    @Override
-    public String getId() {
-        return id;
-    }
-
-    @Override
-    public int getType() {
-        return type;
-    }
-
-    @Nullable
-    @Override
-    public String getName() {
-        return name;
-    }
-
-    @Override
-    public List<String> getChannels() {
-        return channels;
-    }
-
-    @Override
-    public String toString() {
-        return "Lineup{"
-                + "id=" + id + ", "
-                + "type=" + type + ", "
-                + "name=" + name + ", "
-                + "channels=" + channels
-                + "}";
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (o == this) {
-            return true;
-        }
-        if (o instanceof Lineup) {
-            Lineup that = (Lineup) o;
-            return (this.id.equals(that.getId()))
-                    && (this.type == that.getType())
-                    && ((this.name == null) ? (that.getName() == null) : this.name.equals(that.getName()))
-                    && (this.channels.equals(that.getChannels()));
-        }
-        return false;
-    }
-
-    @Override
-    public int hashCode() {
-        int h$ = 1;
-        h$ *= 1000003;
-        h$ ^= id.hashCode();
-        h$ *= 1000003;
-        h$ ^= type;
-        h$ *= 1000003;
-        h$ ^= (name == null) ? 0 : name.hashCode();
-        h$ *= 1000003;
-        h$ ^= channels.hashCode();
-        return h$;
-    }
-
-}
diff --git a/partner_support/src/com/google/android/tv/partner/support/BaseCustomization.java b/partner_support/src/com/google/android/tv/partner/support/BaseCustomization.java
index e40d90d..1f7198e 100644
--- a/partner_support/src/com/google/android/tv/partner/support/BaseCustomization.java
+++ b/partner_support/src/com/google/android/tv/partner/support/BaseCustomization.java
@@ -83,7 +83,14 @@
                             ? 0
                             : res.getIdentifier(resourceName, RES_TYPE_BOOLEAN, packageName);
             if (DEBUG) {
-                Log.d(TAG, "Boolean resource  " + resourceName + " has  " + resId);
+                Log.d(
+                        TAG,
+                        "Boolean resource  "
+                                + resourceName
+                                + " has  "
+                                + resId
+                                + " with value "
+                                + (resId == 0 ? "missing" : res.getBoolean(resId)));
             }
             return resId == 0 ? Optional.empty() : Optional.of(res.getBoolean(resId));
         } catch (PackageManager.NameNotFoundException e) {
diff --git a/partner_support/src/com/google/android/tv/partner/support/EpgInput.java b/partner_support/src/com/google/android/tv/partner/support/EpgInput.java
index 82cc463..20b3542 100644
--- a/partner_support/src/com/google/android/tv/partner/support/EpgInput.java
+++ b/partner_support/src/com/google/android/tv/partner/support/EpgInput.java
@@ -17,13 +17,14 @@
 package com.google.android.tv.partner.support;
 
 import android.content.ContentValues;
+import com.google.auto.value.AutoValue;
 
 /**
  * Value class representing a TV Input that uses Live TV EPG.
  *
  * @see {@link EpgContract.EpgInputs}
  */
-// TODO(b/72052568): Get autovalue to work in aosp master
+@AutoValue
 public abstract class EpgInput {
 
     public static EpgInput createEpgChannel(long id, String inputId, String lineupId) {
diff --git a/partner_support/src/com/google/android/tv/partner/support/EpgInputs.java b/partner_support/src/com/google/android/tv/partner/support/EpgInputs.java
index 53485ec..dddcd08 100644
--- a/partner_support/src/com/google/android/tv/partner/support/EpgInputs.java
+++ b/partner_support/src/com/google/android/tv/partner/support/EpgInputs.java
@@ -59,6 +59,8 @@
                 result.add(EpgInput.createEpgChannel(contentValues));
             }
             return result;
+        } catch (Exception e) {
+            return Collections.emptySet();
         }
     }
 
diff --git a/partner_support/src/com/google/android/tv/partner/support/Lineup.java b/partner_support/src/com/google/android/tv/partner/support/Lineup.java
index 6123eeb..c5d3046 100644
--- a/partner_support/src/com/google/android/tv/partner/support/Lineup.java
+++ b/partner_support/src/com/google/android/tv/partner/support/Lineup.java
@@ -18,12 +18,13 @@
 
 import android.content.ContentValues;
 import android.support.annotation.Nullable;
+import com.google.auto.value.AutoValue;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
 /** Value class for {@link com.google.android.tv.partner.support.EpgContract.Lineups} */
-// TODO(b/72052568): Get autovalue to work in aosp master
+@AutoValue
 public abstract class Lineup {
     /** Lineup type for cable. */
     public static final int LINEUP_CABLE = 0;
diff --git a/partner_support/src/com/google/android/tv/partner/support/PartnerCustomizations.java b/partner_support/src/com/google/android/tv/partner/support/PartnerCustomizations.java
index 7ff168e..1133107 100644
--- a/partner_support/src/com/google/android/tv/partner/support/PartnerCustomizations.java
+++ b/partner_support/src/com/google/android/tv/partner/support/PartnerCustomizations.java
@@ -32,6 +32,11 @@
     public static final String TVPROVIDER_ALLOWS_SYSTEM_INSERTS_TO_PROGRAM_TABLE =
             "tvprovider_allows_system_inserts_to_program_table";
 
+    public static final String TVPROVIDER_ALLOWS_COLUMN_CREATION =
+            "tvprovider_allows_column_creation";
+
+    public static final String TURN_OFF_EMBEDDED_TUNER = "turn_off_embedded_tuner";
+
     public PartnerCustomizations(Context context) {
         super(context, CUSTOMIZE_PERMISSIONS);
     }
@@ -40,4 +45,12 @@
         return getBooleanResource(context, TVPROVIDER_ALLOWS_SYSTEM_INSERTS_TO_PROGRAM_TABLE)
                 .orElse(false);
     }
+
+    public boolean doesTvProviderAllowColumnCreation(Context context) {
+        return getBooleanResource(context, TVPROVIDER_ALLOWS_COLUMN_CREATION).orElse(false);
+    }
+
+    public boolean turnOffEmbeddedTuner(Context context) {
+        return getBooleanResource(context, TURN_OFF_EMBEDDED_TUNER).orElse(false);
+    }
 }
diff --git a/partner_support/tests/robotests/javatests/com/google/android/tv/partner/support/BaseCustomizationTest.java b/partner_support/tests/robotests/javatests/com/google/android/tv/partner/support/BaseCustomizationTest.java
index a6292e3..e217058 100644
--- a/partner_support/tests/robotests/javatests/com/google/android/tv/partner/support/BaseCustomizationTest.java
+++ b/partner_support/tests/robotests/javatests/com/google/android/tv/partner/support/BaseCustomizationTest.java
@@ -25,12 +25,12 @@
 import android.content.pm.PackageManager;
 import com.android.tv.testing.TestSingletonApp;
 import com.android.tv.testing.constants.ConfigConstants;
-import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner;
 import java.util.ArrayList;
 import java.util.List;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Config;
 
@@ -38,15 +38,8 @@
 
 // TODO: move to partner-support
 
-@RunWith(GoogleRobolectricTestRunner.class)
-@Config(
-    manifest =
-            "//third_party/java_src/android_app/live_channels/common/src"
-                    + "/com/android/tv/common"
-                    + ":common/AndroidManifest.xml",
-    sdk = ConfigConstants.SDK,
-    application = TestSingletonApp.class
-)
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK, application = TestSingletonApp.class)
 public class BaseCustomizationTest {
 
     private static final String[] PERMISSIONS = {"com.example.permission"};
diff --git a/proguard.flags b/proguard.flags
index 69b1786..b3795d6 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -68,3 +68,6 @@
 
 # Grpc used by epg via reflection
 -keep class io.grpc.internal.DnsNameResolverProvider
+
+# Don't warn about checkerframework in Android proguard
+-dontwarn org.checkerframework.**
diff --git a/res/drawable-xhdpi/bg_protection.png b/res/drawable-xhdpi/bg_protection.png
deleted file mode 100644
index 02df25a..0000000
--- a/res/drawable-xhdpi/bg_protection.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_store.png b/res/drawable-xhdpi/ic_app_store.png
similarity index 100%
rename from res/drawable-xhdpi/ic_store.png
rename to res/drawable-xhdpi/ic_app_store.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_check_circle_white_48dp.png b/res/drawable-xhdpi/ic_check_circle_white_48dp.png
deleted file mode 100644
index a1cf83e..0000000
--- a/res/drawable-xhdpi/ic_check_circle_white_48dp.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_developer_mode_tv_white_48dp.png b/res/drawable-xhdpi/ic_developer_mode_tv_white_48dp.png
deleted file mode 100644
index 594af85..0000000
--- a/res/drawable-xhdpi/ic_developer_mode_tv_white_48dp.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_error_outline_pink_24dp.png b/res/drawable-xhdpi/ic_error_outline_pink_24dp.png
new file mode 100644
index 0000000..d48bece
--- /dev/null
+++ b/res/drawable-xhdpi/ic_error_outline_pink_24dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_error_white_48dp.png b/res/drawable-xhdpi/ic_error_white_48dp.png
deleted file mode 100644
index 8c2cf1e..0000000
--- a/res/drawable-xhdpi/ic_error_white_48dp.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_live_channels.png b/res/drawable-xhdpi/ic_live_channels.png
deleted file mode 100644
index bb1c2d9..0000000
--- a/res/drawable-xhdpi/ic_live_channels.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_tv_app.png b/res/drawable-xhdpi/ic_tv_app.png
new file mode 100644
index 0000000..c061bf0
--- /dev/null
+++ b/res/drawable-xhdpi/ic_tv_app.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_live_channels_96x96.png b/res/drawable-xhdpi/ic_tv_app_96x96.png
similarity index 100%
rename from res/drawable-xhdpi/ic_live_channels_96x96.png
rename to res/drawable-xhdpi/ic_tv_app_96x96.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_warning_white_18dp.png b/res/drawable-xhdpi/ic_warning_white_18dp.png
deleted file mode 100644
index 13d573e..0000000
--- a/res/drawable-xhdpi/ic_warning_white_18dp.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_warning_white_96dp.png b/res/drawable-xhdpi/ic_warning_white_96dp.png
deleted file mode 100644
index 50d1f29..0000000
--- a/res/drawable-xhdpi/ic_warning_white_96dp.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_warning_yellow_24dp.png b/res/drawable-xhdpi/ic_warning_yellow_24dp.png
new file mode 100644
index 0000000..accb061
--- /dev/null
+++ b/res/drawable-xhdpi/ic_warning_yellow_24dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/banner.png b/res/drawable-xhdpi/live_tv_banner.png
similarity index 100%
rename from res/drawable-xhdpi/banner.png
rename to res/drawable-xhdpi/live_tv_banner.png
Binary files differ
diff --git a/res/drawable-xhdpi/usb_antenna.png b/res/drawable-xhdpi/usb_antenna.png
deleted file mode 100644
index ca5b2d7..0000000
--- a/res/drawable-xhdpi/usb_antenna.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/menu_background.xml b/res/drawable/menu_background.xml
index bdd55c8..defee88 100644
--- a/res/drawable/menu_background.xml
+++ b/res/drawable/menu_background.xml
@@ -14,8 +14,13 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-
-<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
-    android:src="@drawable/bg_protection"
-    android:tileModeX="repeat"
-    android:tileModeY="mirror" />
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+  <!-- 0% to 20%black  half way down then 100% black-->
+  <gradient
+      android:startColor="#00000000"
+      android:endColor="#000000"
+      android:centerColor="#a0000000"
+      android:centerY=".5"
+      android:angle="270" />
+</shape>
diff --git a/res/layout/channel_banner.xml b/res/layout/channel_banner.xml
index 3f105fe..4d3cc24 100644
--- a/res/layout/channel_banner.xml
+++ b/res/layout/channel_banner.xml
@@ -83,13 +83,22 @@
             android:layout_toEndOf="@id/anchor"
             android:visibility="gone" />
 
+        <ImageView android:id="@+id/channel_signal_strength"
+            android:layout_width="@dimen/channel_banner_input_logo_size"
+            android:layout_height="@dimen/channel_banner_input_logo_size"
+            android:layout_marginEnd="8dp"
+            android:layout_marginBottom="-2dp"
+            android:layout_alignBottom="@id/anchor"
+            android:layout_toEndOf="@id/tvinput_logo"
+            android:visibility="gone" />
+
         <TextView android:id="@+id/channel_name"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_marginEnd="12dp"
             android:layout_marginBottom="-4sp"
             android:layout_alignBottom="@id/anchor"
-            android:layout_toEndOf="@id/tvinput_logo"
+            android:layout_toEndOf="@id/channel_signal_strength"
             android:singleLine="true"
             android:ellipsize="end"
             android:maxWidth="@dimen/channel_name_max_width"
diff --git a/res/layout/dvr_details_description.xml b/res/layout/dvr_details_description.xml
index d55688b..ee74952 100644
--- a/res/layout/dvr_details_description.xml
+++ b/res/layout/dvr_details_description.xml
@@ -42,6 +42,27 @@
         android:orientation="vertical"
         android:background="?android:attr/selectableItemBackground">
 
+        <LinearLayout android:id="@+id/dvr_details_description_error_message"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:visibility="gone">
+
+            <ImageView android:layout_width="30dp"
+                android:layout_height="27dp"
+                android:paddingTop="9dp"
+                android:paddingLeft="12dp"
+                android:src="@drawable/ic_error_outline_pink_24dp"/>
+
+            <TextView android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:paddingTop="9dp"
+                android:paddingRight="12dp"
+                android:textColor="@color/dvr_recording_failed_text_color"
+                android:text="@string/dvr_recording_failed"
+                style="?attr/detailsDescriptionBodyStyle" />
+        </LinearLayout>
+
         <TextView android:id="@+id/dvr_details_description_body"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
diff --git a/res/layout/dvr_recording_card_view.xml b/res/layout/dvr_recording_card_view.xml
index 3e95351..3bf9bf6 100644
--- a/res/layout/dvr_recording_card_view.xml
+++ b/res/layout/dvr_recording_card_view.xml
@@ -30,7 +30,7 @@
             android:layout_gravity="center_horizontal"
             android:scaleType="centerCrop"
             android:contentDescription="@null"
-            tv:layout_viewType="main" />
+            tv:layout_viewType="main"/>
 
         <ProgressBar android:id="@+id/recording_progress"
             style="@android:style/Widget.ProgressBar.Horizontal"
@@ -42,21 +42,7 @@
             android:indeterminate="false"
             android:visibility="gone"
             android:max="100"
-            android:layout_gravity="bottom" />
-
-        <FrameLayout android:id="@+id/affiliated_icon_container"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:background="@drawable/card_image_gradient"
-            android:visibility="invisible">
-
-            <ImageView android:id="@+id/affiliated_icon"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_gravity="bottom|right"
-                android:layout_margin="12dp" />
-
-        </FrameLayout>
+            android:layout_gravity="bottom"/>
     </FrameLayout>
 
     <LinearLayout android:id="@+id/info_area"
@@ -99,10 +85,17 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content" >
 
+            <ImageView android:id="@+id/content_icon"
+                android:paddingTop="2dp"
+                android:layout_width="13dp"
+                android:layout_height="15dp"
+                android:gravity="start"
+                android:visibility="gone"/>
+
             <TextView android:id="@+id/content_major"
+                android:layout_toEndOf="@+id/content_icon"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:gravity="start"
                 style="@style/dvr_card_view_content_text" />
 
             <TextView android:id="@+id/content_minor"
diff --git a/res/layout/dvr_schedules_item.xml b/res/layout/dvr_schedules_item.xml
index 90e1123..9e9ee6a 100644
--- a/res/layout/dvr_schedules_item.xml
+++ b/res/layout/dvr_schedules_item.xml
@@ -100,14 +100,25 @@
                             android:lines="1"
                             android:textColor="@color/dvr_schedules_item_info"/>
                     </LinearLayout>
-                    <TextView android:id="@+id/conflict_info"
-                        android:layout_width="match_parent"
+
+                    <LinearLayout android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
-                        android:gravity="start"
-                        android:textSize="10sp"
-                        android:layout_marginBottom="@dimen/dvr_schedules_item_conflict_info_bottom_margin"
-                        android:textColor="@color/dvr_schedules_item_info"
-                        android:visibility="gone"/>
+                        android:orientation="horizontal">
+                        <ImageView android:id="@+id/extra_info_icon"
+                            android:layout_width="13dp"
+                            android:layout_height="13dp"
+                            android:paddingTop="2dp"
+                            android:src="@drawable/ic_error_outline_pink_24dp"
+                            android:visibility="gone"/>
+                        <TextView android:id="@+id/extra_info"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:gravity="start"
+                            android:textSize="10sp"
+                            android:layout_marginBottom="@dimen/dvr_schedules_item_conflict_info_bottom_margin"
+                            android:textColor="@color/dvr_schedules_item_info"
+                            android:visibility="gone"/>
+                    </LinearLayout>
                 </LinearLayout>
             </LinearLayout>
 
diff --git a/res/layout/menu_card_down.xml b/res/layout/menu_card_down.xml
new file mode 100644
index 0000000..0ccfc89
--- /dev/null
+++ b/res/layout/menu_card_down.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2015 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<com.android.tv.menu.SimpleCardView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="@dimen/card_layout_width"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:elevation="@dimen/card_elevation_normal"
+    android:focusable="true"
+    android:clickable="true">
+
+    <ImageView
+        android:layout_width="@dimen/card_image_layout_width"
+        android:layout_height="@dimen/card_image_layout_height"
+        android:background="@color/channel_card_guide"
+        android:paddingBottom="16dp"
+        android:paddingEnd="30dp"
+        android:paddingStart="30dp"
+        android:paddingTop="16dp"
+        android:src="@drawable/quantum_ic_arrow_downward_white_36" />
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/card_meta_layout_height"
+        android:paddingStart="@dimen/card_meta_padding_start"
+        android:paddingEnd="@dimen/card_meta_padding_end"
+        android:paddingTop="@dimen/card_meta_padding_top"
+        android:singleLine="true"
+        android:ellipsize="end"
+        android:fontFamily="@string/condensed_font"
+        android:textColor="@color/card_meta_text_color"
+        android:background="@color/guide_card_meta_background"
+        android:text="@string/channels_item_down"
+        android:textSize="12sp" />
+
+</com.android.tv.menu.SimpleCardView>
diff --git a/res/layout/menu_card_up.xml b/res/layout/menu_card_up.xml
new file mode 100644
index 0000000..2ba365e
--- /dev/null
+++ b/res/layout/menu_card_up.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2015 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<com.android.tv.menu.SimpleCardView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="@dimen/card_layout_width"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:elevation="@dimen/card_elevation_normal"
+    android:focusable="true"
+    android:clickable="true">
+
+    <ImageView
+        android:layout_width="@dimen/card_image_layout_width"
+        android:layout_height="@dimen/card_image_layout_height"
+        android:background="@color/channel_card_guide"
+        android:paddingBottom="16dp"
+        android:paddingEnd="30dp"
+        android:paddingStart="30dp"
+        android:paddingTop="16dp"
+        android:src="@drawable/quantum_ic_arrow_upward_white_36" />
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/card_meta_layout_height"
+        android:paddingStart="@dimen/card_meta_padding_start"
+        android:paddingEnd="@dimen/card_meta_padding_end"
+        android:paddingTop="@dimen/card_meta_padding_top"
+        android:singleLine="true"
+        android:ellipsize="end"
+        android:fontFamily="@string/condensed_font"
+        android:textColor="@color/card_meta_text_color"
+        android:background="@color/guide_card_meta_background"
+        android:text="@string/channels_item_up"
+        android:textSize="12sp" />
+
+</com.android.tv.menu.SimpleCardView>
diff --git a/res/layout/pin_dialog.xml b/res/layout/pin_dialog.xml
index 5071717..d40d70e 100644
--- a/res/layout/pin_dialog.xml
+++ b/res/layout/pin_dialog.xml
@@ -35,7 +35,8 @@
         android:textColor="@color/pin_dialog_text_color"
         android:fontFamily="@string/font"
         android:visibility="invisible"
-        android:singleLine="false"/>
+        android:singleLine="false"
+        android:focusableInTouchMode="true"/>
 
     <LinearLayout
         android:id="@+id/enter_pin"
@@ -54,36 +55,14 @@
             android:fontFamily="@string/font"
             android:singleLine="false" />
 
-        <LinearLayout
+        <com.android.tv.dialog.picker.PinPicker
+            android:id="@+id/pin_picker"
+            android:importantForAccessibility="yes"
             android:layout_width="match_parent"
-            android:layout_height="144dp"
+            android:layout_height="154dp"
             android:paddingStart="24dp"
             android:paddingEnd="24dp"
             android:gravity="center"
-            android:orientation="horizontal">
-
-            <view class="com.android.tv.dialog.PinDialogFragment$PinNumberPicker"
-                android:id="@+id/first"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content" />
-
-            <view class="com.android.tv.dialog.PinDialogFragment$PinNumberPicker"
-                android:id="@+id/second"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_marginStart="8dp" />
-
-            <view class="com.android.tv.dialog.PinDialogFragment$PinNumberPicker"
-                android:id="@+id/third"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_marginStart="8dp" />
-
-            <view class="com.android.tv.dialog.PinDialogFragment$PinNumberPicker"
-                android:id="@+id/fourth"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_marginStart="8dp" />
-        </LinearLayout>
+            />
     </LinearLayout>
 </FrameLayout>
diff --git a/res/layout/pin_number_picker.xml b/res/layout/pin_number_picker.xml
deleted file mode 100644
index 8e8de9f..0000000
--- a/res/layout/pin_number_picker.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2015 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="48dp"
-    android:layout_height="144dp">
-
-    <TextView android:id="@+id/focused_background"
-        android:layout_width="@dimen/pin_number_picker_text_view_width"
-        android:layout_height="@dimen/pin_number_picker_text_view_height"
-        android:layout_gravity="center"
-        android:gravity="center"
-        android:textSize="@dimen/pin_number_picker_text_size"
-        android:textColor="@color/pin_number_picker_text_color"
-        android:fontFamily="@string/light_font"
-        android:background="@drawable/pin_number_picker_focused_background" />
-
-    <LinearLayout
-        android:id="@+id/number_view_holder"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center"
-        android:focusable="true"
-        android:orientation="vertical">
-
-        <TextView android:id="@+id/previous2_number"
-            style="@style/pin_number_view"/>
-        <TextView android:id="@+id/previous_number"
-            style="@style/pin_number_view"/>
-        <TextView android:id="@+id/current_number"
-            style="@style/pin_number_view"/>
-        <TextView android:id="@+id/next_number"
-            style="@style/pin_number_view"/>
-        <TextView android:id="@+id/next2_number"
-            style="@style/pin_number_view"/>
-    </LinearLayout>
-
-</FrameLayout>
diff --git a/res/layout/tunable_tv_view.xml b/res/layout/tunable_tv_view.xml
index 549d053..00c9908 100644
--- a/res/layout/tunable_tv_view.xml
+++ b/res/layout/tunable_tv_view.xml
@@ -17,27 +17,6 @@
 
 <merge xmlns:android="http://schemas.android.com/apk/res/android" >
 
-    <View android:id="@+id/channel_up"
-        android:layout_width="wrap_content"
-        android:focusable="false"
-        android:focusableInTouchMode="true"
-        android:layout_height="1dp"
-        android:layout_gravity="top" />
-    <View android:id="@+id/placeholder"
-        android:layout_width="1dp"
-        android:layout_height="1dp"
-        android:focusable="false"
-        android:focusableInTouchMode="true"
-        android:focusedByDefault="true"
-        android:layout_gravity="center" />
-
-    <View android:id="@+id/channel_down"
-        android:layout_width="wrap_content"
-        android:focusable="false"
-        android:focusableInTouchMode="true"
-        android:layout_height="1dp"
-        android:layout_gravity="bottom" />
-
     <com.android.tv.ui.AppLayerTvView android:id="@+id/tv_view"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
index e57b054..0604dd2 100644
--- a/res/values/arrays.xml
+++ b/res/values/arrays.xml
@@ -29,31 +29,130 @@
     <!-- The category strings to be displayed in the channel guide.
          This list should be synced the data in src/com/android/tv/data/GenreItems.java -->
     <eat-comment />
-    <!-- Genre list [CHAR LIMIT=20] -->
+    <!-- Genre list [CHAR LIMIT=25] -->
     <string-array name="genre_labels" translatable="true">
+        <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+            genres.
+            "All channels", implies all channels will be shown in the program guide.
+            [CHAR LIMIT=25] -->
         <item>All channels</item>
+        <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+            genres.
+            "Family/Kids", implies only channels with "Family/Kids" programs will be shown in the
+            guide.
+            "Family/Kids" programs, are programs designed for families and safe for viewing by
+            young children.
+            [CHAR LIMIT=25] -->
         <item>Family/Kids</item>
+        <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+            genres.
+            "Sports", implies only channels with "Sports" programs will be shown in the guide.
+            "Sports" programs, include sporting events, news and other shows about sports.
+            [CHAR LIMIT=25] -->
         <item>Sports</item>
+        <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+            genres.
+            "Shopping", implies only channels with "Shopping" programs will be shown in the guide.
+            "Shopping" programs are TV shows where people can buy or bid on items.
+             [CHAR LIMIT=25] -->
         <item>Shopping</item>
+        <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+            genres.
+            "Movies", implies only channels with "Movies" programs will be shown in the guide.
+             [CHAR LIMIT=25] -->
         <item>Movies</item>
+        <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+           genres.
+           "Comedy", implies only channels with "Comedy" programs will be shown in the guide.
+           "Comedy" programs are generally intended to be humorous or amusing.
+            [CHAR LIMIT=25] -->
         <item>Comedy</item>
+        <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+           genres.
+           "Travel", implies only channels with "Travel" programs will be shown in the guide.
+           "Travel" programs feature popular destination or travel reviews.
+            [CHAR LIMIT=25] -->
         <item>Travel</item>
+        <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+           genres.
+           "Drama", implies only channels with "Drama" programs will be shown in the guide.
+           "Drama" programs are fictional shows, featuring actors.
+            [CHAR LIMIT=25] -->
         <item>Drama</item>
+        <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+          genres.
+          "Education", implies only channels with "Education" programs will be shown in the guide.
+          "Education" programs are designed to teach, either formally or informally.
+           [CHAR LIMIT=25] -->
         <item>Education</item>
+        <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+          genres.
+          "Animal/Wildlife", implies only channels with "Animal/Wildlife" programs will be shown in
+          the guide.
+          "Animal/Wildlife" programs are  about wild animals or pets.
+           [CHAR LIMIT=25] -->
         <item>Animal/Wildlife</item>
+        <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+          genres.
+          "News", implies only channels with "News" programs will be shown in the guide.
+          "News" programs report world or local events as they unfold.
+           [CHAR LIMIT=25] -->
         <item>News</item>
+        <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+          genres.
+          "Gaming", implies only channels with "Gaming" programs will be shown in the guide.
+          "Gaming" programs rare about games, including video games and board games,
+          but excluding sports.
+           [CHAR LIMIT=25] -->
         <item>Gaming</item>
+        <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+            genres.
+            "Arts", implies only channels with "Arts" programs will be shown in the guide.
+            "Arts" programs about artistic endeavours or events like dance, music theater, drawing.
+            [CHAR LIMIT=25] -->
         <item>Arts</item>
+        <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+            genres.
+            "Entertainment", implies only channels with "Entertainment" programs will be shown in
+            the guide.
+            "Entertainment" programs discuss news and the people involved in the entertainment
+            industry.
+            [CHAR LIMIT=25] -->
         <item>Entertainment</item>
+        <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+            genres.
+            "Lifestyle", implies only channels with "Lifestyle" programs will be shown in
+            the guide.
+            "Lifestyle" programs feature topics such as fashion, diet, exercise, health and leisure
+             pursuits.
+            [CHAR LIMIT=25] -->
         <item>Lifestyle</item>
+        <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+            genres.
+            "Music", implies only channels with "Music" programs will be shown in
+            the guide.
+            "Music" programs feature live or recorded music.
+            [CHAR LIMIT=25] -->
         <item>Music</item>
+        <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+            genres.
+            "Premier", implies only channels with "Premier" programs will be shown in
+            the guide.
+            "Premier" programs are available at an extra cost.
+            [CHAR LIMIT=25] -->
         <item>Premier</item>
+        <!-- This is an item in a list to filter channels shown in a TV program guide, based on
+            genres.
+            "Tech/Science", implies only channels with "Tech/Science" programs will be shown in
+            the guide.
+            "Tech/Science" programs are about science or technology .
+            [CHAR LIMIT=25] -->
         <item>Tech/Science</item>
     </string-array>
 
     <!-- Titles in the onboarding page. -->
     <string-array name="welcome_page_titles">
-        <item>Live TV</item>
+        <item><xliff:g id="app_name">Live TV</xliff:g> </item>
         <item>A simple way to discover content</item>
         <item>Download apps, get more channels</item>
         <item>Customize your channel line-up</item>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index e0a0b99..b68feb1 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -158,4 +158,6 @@
     <color name="dvr_guided_step_action_text_color_selected">#111111</color>
     <color name="dvr_detail_default_background">#FF01579B</color>
     <color name="dvr_detail_default_background_scrim">#CC000000</color>
+    <color name="dvr_recording_failed_text_color">#FFCDD2</color>
+    <color name="dvr_recording_conflict_text_color">#FFE082</color>
 </resources>
diff --git a/tuner/tests/TestManifest.xml b/res/values/strings-custom.xml
similarity index 68%
rename from tuner/tests/TestManifest.xml
rename to res/values/strings-custom.xml
index f84aa90..22f7331 100644
--- a/tuner/tests/TestManifest.xml
+++ b/res/values/strings-custom.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2018 The Android Open Source Project
+  ~ Copyright (C) 2019 The Android Open Source Project
   ~
   ~ Licensed under the Apache License, Version 2.0 (the "License");
   ~ you may not use this file except in compliance with the License.
@@ -14,10 +14,9 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
+<resources>
 
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.tv.tuner.tests">
-  <!-- android_local_test needs minSdkVersion set -->
-  <uses-sdk  android:minSdkVersion="23"/>
+    <!-- Name of application [CHAR LIMIT=NONE] -->
+    <string name="app_name" translatable="false">Live TV</string>
 
-</manifest>
\ No newline at end of file
+</resources>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index cea4ee6..3682475 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -26,21 +26,18 @@
 
     <string name="option_item_divider_font" translatable="false">@string/condensed_font</string>
 
-    <!-- Name of application [CHAR LIMIT=NONE] -->
-    <string name="app_name">Live TV</string>
-
     <!-- Title of an application permission, listed so the user can choose
         whether they want to allow the application to do this. -->
-    <string name="permlab_receiveInputEvent" translatable="false">receive input events from Live TV app</string>
+    <string name="permlab_receiveInputEvent" translatable="false">receive input events from <xliff:g id="app_name">Live TV</xliff:g>  app</string>
     <!-- Description of an application permission, listed so the user can choose
         whether they want to allow the application to do this. -->
-    <string name="permdesc_receiveInputEvent" translatable="false">Allows the app to receive input events from Live TV app</string>
+    <string name="permdesc_receiveInputEvent" translatable="false">Allows the app to receive input events from <xliff:g id="app_name">Live TV</xliff:g>  app</string>
     <!-- Title of an application permission, listed so the user can choose
         whether they want to allow the application to do this. -->
-    <string name="permlab_customizeTvApp" translatable="false">customize Live TV app</string>
+    <string name="permlab_customizeTvApp" translatable="false">customize <xliff:g id="app_name">Live TV</xliff:g>  app</string>
     <!-- Description of an application permission, listed so the user can choose
         whether they want to allow the application to do this. -->
-    <string name="permdesc_customizeTvApp" translatable="false">Allows the app to customize Live TV app</string>
+    <string name="permdesc_customizeTvApp" translatable="false">Allows the app to customize <xliff:g id="app_name">Live TV</xliff:g>  app</string>
 
     <!-- Program information, mainly used for channel banner and program guide. -->
     <eat-comment />
@@ -90,6 +87,12 @@
 
     <!-- Label of Program guide item in the channel list row. [CHAR LIMIT=23] -->
     <string name="channels_item_program_guide">Program guide</string>
+    <!-- Label of the item in the "channel menu" that changes the current TV channel in the "up"
+         direction, to the next larger channel number. [CHAR LIMIT=23] -->
+    <string name="channels_item_up">Channel up</string>
+    <!-- Label of the item in the "channel menu" that changes the current TV channel in the "down"
+         direction, to the next smaller channel number. [CHAR LIMIT=23] -->
+    <string name="channels_item_down">Channel down</string>
     <!-- Label of setup item in the channel list row. The item is shown only
          when new inputs are installed. [CHAR LIMIT=23] -->
     <string name="channels_item_setup">New channels available</string>
@@ -366,10 +369,22 @@
     <eat-comment />
     <!-- Description on the locked screen when current channel is locked by parental control. [CHAR LIMIT=NONE] -->
     <string name="tvview_channel_locked">To watch this channel, press Right and enter your PIN</string>
+    <!-- Description on the locked screen when current channel is locked by parental control and talk back is turned on.
+        "press select" refers to a button on the remote.  [CHAR LIMIT=NONE] -->
+    <string name="tvview_channel_locked_talkback">To watch this channel, press select and enter your PIN</string>
     <!-- Description on the locked screen when the rating of the current content is restricted by parental control. [CHAR LIMIT=NONE] -->
     <string name="tvview_content_locked">To watch this program, press Right and enter your PIN</string>
+    <!-- Description on the locked screen when the rating of the current content is restricted by parental control and talk back is turned on.
+     "press select" refers to a button on the remote.[CHAR LIMIT=NONE] -->
+    <string name="tvview_content_locked_talkback">To watch this program, press select and enter your PIN</string>
     <!-- Description on the locked screen when the current content is unrated and it's restricted by parental control. [CHAR LIMIT=NONE] -->
     <string name="tvview_content_locked_unrated">This program is unrated.\nTo watch this program, press Right and enter your PIN</string>
+    <!-- Description on the locked screen when the current content is unrated and it's restricted by parental control and talk back is turned on.
+    "press select" refers to a button on the remote.[CHAR LIMIT=NONE] -->
+    <string name="tvview_content_locked_unrated_talkback">This program is unrated.\nTo watch this program, press select and enter your PIN</string>
+    <!-- Description on the locked screen with the rating when the rating of the current content is restricted by parental control and talk back is turned on. [CHAR LIMIT=NONE]
+     "press select" refers to a button on the remote.-->
+    <string name="tvview_content_locked_format_talkback">This program is rated <xliff:g id="rating" example="TV_MA">%1$s</xliff:g>.\nTo watch this program, press select and enter your PIN.</string>
     <!-- Description on the locked screen with the rating when the rating of the current content is restricted by parental control. [CHAR LIMIT=NONE] -->
     <string name="tvview_content_locked_format">This program is rated <xliff:g id="rating" example="TV_MA">%1$s</xliff:g>.\nTo watch this program, press Right and enter your PIN.</string>
     <!-- Description on the locked screen when current channel is locked by parental control. [CHAR LIMIT=NONE] -->
@@ -464,7 +479,7 @@
          the source video through to the display and don't provide ability to tune to a specific
          channel unless the user directly controls the external source device (e.g. game console,
          DVD player, settop box, etc) that is connected to the TV. [CHAR LIMIT=NONE] -->
-    <string name="msg_not_passthrough_input">Tuner type not suitable. Please launch Live TV app for tuner type TV input.</string>
+    <string name="msg_not_passthrough_input">Tuner type not suitable. Please launch <xliff:g id="app_name">Live TV</xliff:g>  app for tuner type TV input.</string>
     <!-- Error message when tune is failed. [CHAR LIMIT=NONE] -->
     <string name="msg_tune_failed">Tune failed</string>
     <!-- Error message when the user attempts an action (select TIS setup-activity, app-link,
@@ -475,11 +490,13 @@
     <string name="msg_all_channels_hidden">All source channels are hidden.\nSelect at least one channel to watch.</string>
     <!-- Message displayed when availability is changed by unknown reason. [CHAR LIMIT=NONE] -->
     <string name="msg_channel_unavailable_unknown">The video is unexpectedly unavailable</string>
+    <!-- Message displayed when a TV input (eg HDMI Cable) is not physically connected. [CHAR LIMIT=NONE] -->
+    <string name="msg_channel_unavailable_not_connected">No Signal. Please check your source connection.</string>
     <!-- Message to notify the different use of Back Button: Home Button(To exit) Back button
          (commands for external device)  [CHAR LIMIT=NONE] -->
     <string name="msg_back_key_guide">BACK key is for connected device. Press HOME button to exit.</string>
     <!-- Error message when a user denied to grant READ_TV_LISTING permission. [CHAR LIMIT=NONE] -->
-    <string name="msg_read_tv_listing_permission_denied">Live TV needs permission to read the TV listings.</string>
+    <string name="msg_read_tv_listing_permission_denied"><xliff:g id="app_name">Live TV</xliff:g>  needs permission to read the TV listings.</string>
 
     <!-- Strings for debug or not to be shown to users -->
     <eat-comment />
@@ -505,13 +522,13 @@
     <string name="dvr_history_dialog_title" translatable="false">DVR history</string>
 
     <!-- Display name of DVR recording service's notification channel. -->
-    <string name="dvr_notification_channel_name" translatable="false">Live TV DVR</string>
+    <string name="dvr_notification_channel_name" translatable="false"><xliff:g id="app_name">Live TV</xliff:g>  DVR</string>
     <!-- Content title of DVR recording service's notification. -->
-    <string name="dvr_notification_content_title" translatable="false">Live TV DVR</string>
+    <string name="dvr_notification_content_title" translatable="false"><xliff:g id="app_name">Live TV</xliff:g>  DVR</string>
     <!-- Content text of DVR recording service's notification during recording. -->
-    <string name="dvr_notification_content_text_recording" translatable="false">Live TV are recording.</string>
+    <string name="dvr_notification_content_text_recording" translatable="false"><xliff:g id="app_name">Live TV</xliff:g>  are recording.</string>
     <!-- Content text of DVR recording service's notification during updating schedules. -->
-    <string name="dvr_notification_content_text_loading" translatable="false">Live TV are updating recording schedules.</string>
+    <string name="dvr_notification_content_text_loading" translatable="false"><xliff:g id="app_name">Live TV</xliff:g>  are updating recording schedules.</string>
 
     <!-- Default content title of tuner installing notifications. -->
     <string name="tuner_install_notification_content_title" translatable="false">Install <xliff:g id="tuner_package" example="Tuner package">%s</xliff:g></string>
@@ -577,7 +594,11 @@
     <!-- Description of a card view to show full list of scheduled recordings. [CHAR LIMIT=25] -->
     <string name="dvr_full_schedule_card_view_title">Full schedule</string>
     <!-- Description of failed recordings. [CHAR LIMIT=25] -->
-    <string name="dvr_recording_failed">Recording Failed</string>
+    <string name="dvr_recording_failed">Recording Failed.</string>
+    <!-- Description of failed recordings. [CHAR LIMIT=25] -->
+    <string name="dvr_recording_failed_no_period">Recording Failed</string>
+    <!-- Description of recording conflicts. [CHAR LIMIT=25] -->
+    <string name="dvr_recording_conflict">Recording Conflict</string>
     <!-- Description of how many following days the schedule list will show. [CHAR LIMIT=25] -->
     <plurals name="dvr_full_schedule_card_view_content">
         <item quantity="one">Next %1$d day</item>
@@ -607,6 +628,8 @@
 
     <!-- DVR detailed page -->
     <eat-comment />
+    <!-- Button label to schedule a recording. -->
+    <string name="dvr_detail_schedule_recording">Schedule recording</string>
     <!-- Button label to cancel the recording schedule. -->
     <string name="dvr_detail_cancel_recording">Cancel recording</string>
     <!-- Button label to stop the current recording. -->
@@ -631,6 +654,18 @@
     <string name="dvr_detail_view_schedule">View schedule</string>
     <!-- Text label to indicate there's more text in the details description [CHAR LIMIT=20] -->
     <string name="dvr_detail_read_more">Read more</string>
+    <!-- Description of failed recordings caused by system failures. -->
+    <string name="dvr_recording_failed_system_failure">System failure. (Error code: <xliff:g id="errorCode" example="0">%1$d</xliff:g>)</string>
+    <!-- Description of failed recordings when they are not started correctly. -->
+    <string name="dvr_recording_failed_not_started">Recording was not started. Please check the antenna and hard drive connections (if any).</string>
+    <!-- Description of failed recordings when required resource is busy. -->
+    <string name="dvr_recording_failed_resource_busy">Failed to tune to the channel. Possible causes: weak signal, poor antenna connection, or recording conflict.</string>
+    <!-- Description of failed recordings when the input is unavailable. -->
+    <string name="dvr_recording_failed_input_unavailable"><xliff:g id="inputId" example="com.example.partnersupportsampletvinput/.SampleTvInputService">%1$s</xliff:g> unavailable.</string>
+    <!-- Description of failed recordings when the input doesn't support recording. -->
+    <string name="dvr_recording_failed_input_dvr_unsupported">Recording of this channel is not supported.</string>
+    <!-- Description of failed recordings when the space is insufficient. -->
+    <string name="dvr_recording_failed_insufficient_space">Insufficient space. Please connect an external storage device or delete some existing recordings.</string>
 
 
     <!-- DVR series settings -->
@@ -754,7 +789,7 @@
          sufficient space.-->
     <string name="dvr_error_insufficient_space_description_three_or_more_recordings">The recordings of <xliff:g id="programName_1" example="Friends">%1$s</xliff:g>, <xliff:g id="programName_2" example="Friends">%2$s</xliff:g> and <xliff:g id="programName_3" example="Friends">%3$s</xliff:g> didn\'t complete due to insufficient storage.</string>
     <!-- Dialog title which will be shown when the current storage is too small for DVR. -->
-    <string name="dvr_error_small_sized_storage_title">More stroage needed</string>
+    <string name="dvr_error_small_sized_storage_title">More storage needed</string>
     <!-- Dialog description which will be shown when the current storage is too small for DVR. -->
     <string name="dvr_error_small_sized_storage_description">You will be able to record programs. However there is not enough storage on your device to start recording. Please connect an external drive that is <xliff:g id="storage_size" example="10GB">%1$d</xliff:g>GB or larger and follow the steps to format it as device storage.</string>
     <!-- Dialog title which will be shown when there is no free space on the current storage for DVR. -->
@@ -930,6 +965,20 @@
         <item quantity="other">(%1$d minutes)</item>
     </plurals>
 
+    <!-- DVR history list strings -->
+    <!-- Short description of failed recordings. -->
+    <string name="dvr_recording_failed_short">Failed.</string>
+    <!-- Short description of failed recordings when they are not started correctly. -->
+    <string name="dvr_recording_failed_not_started_short">Failed to start recording.</string>
+    <!-- Short description of failed recordings when required resource is busy. -->
+    <string name="dvr_recording_failed_resource_busy_short">Failed to tune to the channel.</string>
+    <!-- Short description of failed recordings when the input is unavailable. -->
+    <string name="dvr_recording_failed_input_unavailable_short"><xliff:g id="inputId" example="com.example.partnersupportsampletvinput/.SampleTvInputService">%1$s</xliff:g> unavailable.</string>
+    <!-- Short description of failed recordings when the input doesn't support recording. -->
+    <string name="dvr_recording_failed_input_dvr_unsupported_short">Recording not supported.</string>
+    <!-- Short description of failed recordings when the space is insufficient. -->
+    <string name="dvr_recording_failed_insufficient_space_short">Insufficient space.</string>
+
     <!-- DVR date related strings -->
     <eat-comment/>
     <!-- Date text to represent today. -->
@@ -952,4 +1001,21 @@
     <eat-comment/>
     <!-- Name for recorded programs preview channel -->
     <string name="recorded_programs_preview_channel">Recorded Programs</string>
+
+    <!-- Request Permission -->
+    <eat-comment />
+    <!-- Title of the dialog to show storage permission rationale -->
+    <string name="write_storage_permission_rationale_title">Request permission</string>
+    <!-- The users has asked to delete TV programs they have recorded, however the application
+        first needs a system permission to delete files.
+        This is the text of a dialog that explains to the users they will be asked to grant
+        Live TV permission to delete files.
+        "Allow Live TV to access photos, media, and files on your device?\" is from the text
+        from the system message at
+       https://tc.corp.google.com/btviewer/messagedetail?project=AndroidPlatform&msgId=7885942926944299560
+        -->
+    <string name="write_storage_permission_rationale_description">You will be asked to, \"Allow <xliff:g id="app_name">Live TV</xliff:g> to access photos, media, and files on your device?\"\n
+        Selecting \"Allow\", enables <xliff:g id="app_name">Live TV</xliff:g> to immediately
+        free storage space when deleting recorded TV programs.
+        This makes more space available for new recordings.</string>
 </resources>
diff --git a/res/values/themes.xml b/res/values/themes.xml
index 2165a75..9653707 100644
--- a/res/values/themes.xml
+++ b/res/values/themes.xml
@@ -77,7 +77,6 @@
         <item name="guidanceIconStyle">@style/TV.Dvr.GuidanceIconStyle</item>
         <item name="guidedActionsListStyle">@style/TV.Dvr.GuidedActionsListStyle</item>
         <item name="guidedActionItemContainerStyle">@style/TV.Dvr.GuidedActionItemContainerStyle</item>
-        <item name="guidedActionContentWidthWeight">@string/lb_guidedactions_width_weight</item>
     </style>
 
     <style name="Theme.TV.Dvr.GuidedStep.Twoline.Action" parent = "Theme.TV.Dvr.GuidedStep">
@@ -120,4 +119,4 @@
              clicking DVR cards overlapping with fragment transition. -->
         <item name="android:windowAllowEnterTransitionOverlap">false</item>
     </style>
-</resources>
\ No newline at end of file
+</resources>
diff --git a/src/com/android/tv/util/Filter.java b/settings.gradle
similarity index 67%
rename from src/com/android/tv/util/Filter.java
rename to settings.gradle
index 3e24a49..6d5cb54 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/settings.gradle
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,10 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.tv.util;
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
-}
+/*
+ * Experimental gradle configuration.  This file may not be up to date.
+ */
+
+include ':common'
+include ':tuner'
+include ':SampleDvbTuner'
+project(":SampleDvbTuner").projectDir = file("tuner/SampleDvbTuner")
diff --git a/src/com/android/tv/AudioManagerHelper.java b/src/com/android/tv/AudioManagerHelper.java
deleted file mode 100644
index 942d431..0000000
--- a/src/com/android/tv/AudioManagerHelper.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tv;
-
-import android.app.Activity;
-import android.content.Context;
-import android.media.AudioManager;
-import android.os.Build;
-import com.android.tv.receiver.AudioCapabilitiesReceiver;
-import com.android.tv.ui.TunableTvView;
-import com.android.tv.ui.TunableTvViewPlayingApi;
-
-/** A helper class to help {@link MainActivity} to handle audio-related stuffs. */
-class AudioManagerHelper implements AudioManager.OnAudioFocusChangeListener {
-    private static final float AUDIO_MAX_VOLUME = 1.0f;
-    private static final float AUDIO_MIN_VOLUME = 0.0f;
-    private static final float AUDIO_DUCKING_VOLUME = 0.3f;
-
-    private final Activity mActivity;
-    private final TunableTvViewPlayingApi mTvView;
-    private final AudioManager mAudioManager;
-    private final AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
-
-    private boolean mAc3PassthroughSupported;
-    private int mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS;
-
-    AudioManagerHelper(Activity activity, TunableTvViewPlayingApi tvView) {
-        mActivity = activity;
-        mTvView = tvView;
-        mAudioManager = (AudioManager) activity.getSystemService(Context.AUDIO_SERVICE);
-        mAudioCapabilitiesReceiver =
-                new AudioCapabilitiesReceiver(
-                        activity,
-                        new AudioCapabilitiesReceiver.OnAc3PassthroughCapabilityChangeListener() {
-                            @Override
-                            public void onAc3PassthroughCapabilityChange(boolean capability) {
-                                mAc3PassthroughSupported = capability;
-                            }
-                        });
-        mAudioCapabilitiesReceiver.register();
-    }
-
-    /**
-     * Sets suitable volume to {@link TunableTvView} according to the current audio focus. If the
-     * focus status is {@link AudioManager#AUDIOFOCUS_LOSS} and the activity is under PIP mode, this
-     * method will finish the activity.
-     */
-    void setVolumeByAudioFocusStatus() {
-        if (mTvView.isPlaying()) {
-            switch (mAudioFocusStatus) {
-                case AudioManager.AUDIOFOCUS_GAIN:
-                    if (mTvView.isTimeShiftAvailable()) {
-                        mTvView.timeshiftPlay();
-                    } else {
-                        mTvView.setStreamVolume(AUDIO_MAX_VOLUME);
-                    }
-                    break;
-                case AudioManager.AUDIOFOCUS_LOSS:
-                    if (TvFeatures.PICTURE_IN_PICTURE.isEnabled(mActivity)
-                            && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
-                            && mActivity.isInPictureInPictureMode()) {
-                        mActivity.finish();
-                        break;
-                    }
-                    // fall through
-                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
-                    if (mTvView.isTimeShiftAvailable()) {
-                        mTvView.timeshiftPause();
-                    } else {
-                        mTvView.setStreamVolume(AUDIO_MIN_VOLUME);
-                    }
-                    break;
-                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
-                    if (mTvView.isTimeShiftAvailable()) {
-                        mTvView.timeshiftPause();
-                    } else {
-                        mTvView.setStreamVolume(AUDIO_DUCKING_VOLUME);
-                    }
-                    break;
-            }
-        }
-    }
-
-    /**
-     * Tries to request audio focus from {@link AudioManager} and set volume according to the
-     * returned result.
-     */
-    void requestAudioFocus() {
-        int result =
-                mAudioManager.requestAudioFocus(
-                        this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
-        mAudioFocusStatus =
-                (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
-                        ? AudioManager.AUDIOFOCUS_GAIN
-                        : AudioManager.AUDIOFOCUS_LOSS;
-        setVolumeByAudioFocusStatus();
-    }
-
-    /** Abandons audio focus. */
-    void abandonAudioFocus() {
-        mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS;
-        mAudioManager.abandonAudioFocus(this);
-    }
-
-    /** Returns {@code true} if the device supports AC3 pass-through. */
-    boolean isAc3PassthroughSupported() {
-        return mAc3PassthroughSupported;
-    }
-
-    /** Release the resources the helper class may occupied. */
-    void release() {
-        mAudioCapabilitiesReceiver.unregister();
-    }
-
-    @Override
-    public void onAudioFocusChange(int focusChange) {
-        mAudioFocusStatus = focusChange;
-        setVolumeByAudioFocusStatus();
-    }
-}
diff --git a/src/com/android/tv/util/Filter.java b/src/com/android/tv/ChannelChanger.java
similarity index 67%
copy from src/com/android/tv/util/Filter.java
copy to src/com/android/tv/ChannelChanger.java
index 3e24a49..5503569 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/src/com/android/tv/ChannelChanger.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,11 +13,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package com.android.tv;
 
-package com.android.tv.util;
+/** Changes the channel. */
+public interface ChannelChanger {
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
+    void channelUp();
+
+    void channelDown();
 }
diff --git a/src/com/android/tv/ChannelTuner.java b/src/com/android/tv/ChannelTuner.java
index 8ab145a..fe13898 100644
--- a/src/com/android/tv/ChannelTuner.java
+++ b/src/com/android/tv/ChannelTuner.java
@@ -97,13 +97,7 @@
         mStarted = true;
         mChannelDataManager.addListener(mChannelDataManagerListener);
         if (mChannelDataManager.isDbLoadFinished()) {
-            mHandler.post(
-                    new Runnable() {
-                        @Override
-                        public void run() {
-                            mChannelDataManagerListener.onLoadFinished();
-                        }
-                    });
+            mHandler.post(mChannelDataManagerListener::onLoadFinished);
         }
     }
 
diff --git a/src/com/android/tv/InputSessionManager.java b/src/com/android/tv/InputSessionManager.java
index 4f298ed..ea17751 100644
--- a/src/com/android/tv/InputSessionManager.java
+++ b/src/com/android/tv/InputSessionManager.java
@@ -20,11 +20,8 @@
 import android.content.Context;
 import android.media.tv.TvContentRating;
 import android.media.tv.TvInputInfo;
-import android.media.tv.TvRecordingClient;
-import android.media.tv.TvRecordingClient.RecordingCallback;
 import android.media.tv.TvTrackInfo;
 import android.media.tv.TvView;
-import android.media.tv.TvView.TvInputCallback;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
@@ -36,9 +33,15 @@
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
+import com.android.tv.common.compat.TvRecordingClientCompat;
+import com.android.tv.common.compat.TvRecordingClientCompat.RecordingCallbackCompat;
+import com.android.tv.common.compat.TvViewCompat;
+import com.android.tv.common.compat.TvViewCompat.TvInputCallbackCompat;
 import com.android.tv.data.api.Channel;
+import com.android.tv.dvr.DvrTvView;
 import com.android.tv.ui.TunableTvView;
 import com.android.tv.ui.TunableTvView.OnTuneListener;
+import com.android.tv.ui.api.TunableTvViewPlayingApi;
 import com.android.tv.util.TvInputManagerHelper;
 import java.util.Collections;
 import java.util.List;
@@ -87,7 +90,9 @@
     @MainThread
     @NonNull
     public TvViewSession createTvViewSession(
-            TvView tvView, TunableTvView tunableTvView, TvInputCallback callback) {
+            TvViewCompat tvView,
+            TunableTvViewPlayingApi tunableTvView,
+            TvInputCallbackCompat callback) {
         TvViewSession session = new TvViewSession(tvView, tunableTvView, callback);
         mTvViewSessions.add(session);
         if (DEBUG) Log.d(TAG, "TvView session created: " + session);
@@ -107,7 +112,7 @@
     public RecordingSession createRecordingSession(
             String inputId,
             String tag,
-            RecordingCallback callback,
+            RecordingCallbackCompat callback,
             Handler handler,
             long endTimeMs) {
         RecordingSession session = new RecordingSession(inputId, tag, callback, handler, endTimeMs);
@@ -237,9 +242,10 @@
      */
     @MainThread
     public class TvViewSession {
-        private final TvView mTvView;
-        private final TunableTvView mTunableTvView;
-        private final TvInputCallback mCallback;
+        private final TvViewCompat mTvView;
+        private final TunableTvViewPlayingApi mTunableTvView;
+        private final TvInputCallbackCompat mCallback;
+        private final boolean mIsDvrSession;
         private Channel mChannel;
         private String mInputId;
         private Uri mChannelUri;
@@ -248,10 +254,14 @@
         private boolean mTuned;
         private boolean mNeedToBeRetuned;
 
-        TvViewSession(TvView tvView, TunableTvView tunableTvView, TvInputCallback callback) {
+        TvViewSession(
+                TvViewCompat tvView,
+                TunableTvViewPlayingApi tunableTvView,
+                TvInputCallbackCompat callback) {
             mTvView = tvView;
             mTunableTvView = tunableTvView;
             mCallback = callback;
+            mIsDvrSession = tunableTvView instanceof DvrTvView;
             mTvView.setCallback(
                     new DelegateTvInputCallback(mCallback) {
                         @Override
@@ -338,9 +348,13 @@
 
         void retune() {
             if (DEBUG) Log.d(TAG, "Retune requested.");
+            if (mIsDvrSession) {
+                Log.w(TAG, "DVR session should not call retune()!");
+                return;
+            }
             if (mNeedToBeRetuned) {
                 if (DEBUG) Log.d(TAG, "Retuning: {channel=" + mChannel + "}");
-                mTunableTvView.tuneTo(mChannel, mParams, mOnTuneListener);
+                ((TunableTvView) mTunableTvView).tuneTo(mChannel, mParams, mOnTuneListener);
                 mNeedToBeRetuned = false;
             }
         }
@@ -369,9 +383,13 @@
         void resetByRecording() {
             mCallback.onVideoUnavailable(
                     mInputId, TunableTvView.VIDEO_UNAVAILABLE_REASON_NO_RESOURCE);
+            if (mIsDvrSession) {
+                Log.w(TAG, "DVR session should not call resetByRecording()!");
+                return;
+            }
             if (mTuned) {
                 if (DEBUG) Log.d(TAG, "Reset TvView session by recording");
-                mTunableTvView.resetByRecording();
+                ((TunableTvView) mTunableTvView).resetByRecording();
                 reset();
             }
             mNeedToBeRetuned = true;
@@ -386,22 +404,22 @@
     public class RecordingSession {
         private final String mInputId;
         private Uri mChannelUri;
-        private final RecordingCallback mCallback;
+        private final RecordingCallbackCompat mCallback;
         private final Handler mHandler;
         private volatile long mEndTimeMs;
-        private TvRecordingClient mClient;
+        private TvRecordingClientCompat mClient;
         private boolean mTuned;
 
         RecordingSession(
                 String inputId,
                 String tag,
-                RecordingCallback callback,
+                RecordingCallbackCompat callback,
                 Handler handler,
                 long endTimeMs) {
             mInputId = inputId;
             mCallback = callback;
             mHandler = handler;
-            mClient = new TvRecordingClient(mContext, tag, callback, handler);
+            mClient = new TvRecordingClientCompat(mContext, tag, callback, handler);
             mEndTimeMs = endTimeMs;
         }
 
@@ -409,29 +427,26 @@
             if (DEBUG) Log.d(TAG, "Release of recording session requested.");
             runOnHandler(
                     mMainThreadHandler,
-                    new Runnable() {
-                        @Override
-                        public void run() {
-                            if (DEBUG) Log.d(TAG, "Releasing of recording session.");
-                            mTuned = false;
-                            mClient.release();
-                            mClient = null;
-                            for (TvViewSession session : mTvViewSessions) {
-                                if (DEBUG) {
-                                    Log.d(
-                                            TAG,
-                                            "Finding TvView sessions for retune: {tuned="
-                                                    + session.mTuned
-                                                    + ", inputId="
-                                                    + session.mInputId
-                                                    + ", session="
-                                                    + session
-                                                    + "}");
-                                }
-                                if (!session.mTuned && Objects.equals(session.mInputId, mInputId)) {
-                                    session.retune();
-                                    break;
-                                }
+                    () -> {
+                        if (DEBUG) Log.d(TAG, "Releasing of recording session.");
+                        mTuned = false;
+                        mClient.release();
+                        mClient = null;
+                        for (TvViewSession session : mTvViewSessions) {
+                            if (DEBUG) {
+                                Log.d(
+                                        TAG,
+                                        "Finding TvView sessions for retune: {tuned="
+                                                + session.mTuned
+                                                + ", inputId="
+                                                + session.mInputId
+                                                + ", session="
+                                                + session
+                                                + "}");
+                            }
+                            if (!session.mTuned && Objects.equals(session.mInputId, mInputId)) {
+                                session.retune();
+                                break;
                             }
                         }
                     });
@@ -441,42 +456,39 @@
         public void tune(String inputId, Uri channelUri) {
             runOnHandler(
                     mMainThreadHandler,
-                    new Runnable() {
-                        @Override
-                        public void run() {
-                            int tunedRecordingSessionCount = getTunedRecordingSessionCount(inputId);
-                            TvInputInfo input = mInputManager.getTvInputInfo(inputId);
-                            if (input == null
-                                    || !input.canRecord()
-                                    || input.getTunerCount() <= tunedRecordingSessionCount) {
-                                runOnHandler(
-                                        mHandler,
-                                        new Runnable() {
-                                            @Override
-                                            public void run() {
-                                                mCallback.onConnectionFailed(inputId);
-                                            }
-                                        });
-                                return;
-                            }
-                            mTuned = true;
-                            int tunedTuneSessionCount = getTunedTvViewSessionCount(inputId);
-                            if (!isTunedForTvView(channelUri)
-                                    && tunedTuneSessionCount > 0
-                                    && tunedRecordingSessionCount + tunedTuneSessionCount
-                                            >= input.getTunerCount()) {
-                                for (TvViewSession session : mTvViewSessions) {
-                                    if (session.mTuned
-                                            && Objects.equals(session.mInputId, inputId)
-                                            && !isTunedForRecording(session.mChannelUri)) {
-                                        session.resetByRecording();
-                                        break;
-                                    }
+                    () -> {
+                        int tunedRecordingSessionCount = getTunedRecordingSessionCount(inputId);
+                        TvInputInfo input = mInputManager.getTvInputInfo(inputId);
+                        if (input == null
+                                || !input.canRecord()
+                                || input.getTunerCount() <= tunedRecordingSessionCount) {
+                            runOnHandler(
+                                    mHandler,
+                                    new Runnable() {
+                                        @Override
+                                        public void run() {
+                                            mCallback.onConnectionFailed(inputId);
+                                        }
+                                    });
+                            return;
+                        }
+                        mTuned = true;
+                        int tunedTuneSessionCount = getTunedTvViewSessionCount(inputId);
+                        if (!isTunedForTvView(channelUri)
+                                && tunedTuneSessionCount > 0
+                                && tunedRecordingSessionCount + tunedTuneSessionCount
+                                        >= input.getTunerCount()) {
+                            for (TvViewSession session : mTvViewSessions) {
+                                if (session.mTuned
+                                        && Objects.equals(session.mInputId, inputId)
+                                        && !isTunedForRecording(session.mChannelUri)) {
+                                    session.resetByRecording();
+                                    break;
                                 }
                             }
-                            mChannelUri = channelUri;
-                            mClient.tune(inputId, channelUri);
                         }
+                        mChannelUri = channelUri;
+                        mClient.tune(inputId, channelUri);
                     });
         }
 
@@ -504,10 +516,10 @@
         }
     }
 
-    private static class DelegateTvInputCallback extends TvInputCallback {
-        private final TvInputCallback mDelegate;
+    private static class DelegateTvInputCallback extends TvInputCallbackCompat {
+        private final TvInputCallbackCompat mDelegate;
 
-        DelegateTvInputCallback(TvInputCallback delegate) {
+        DelegateTvInputCallback(TvInputCallbackCompat delegate) {
             mDelegate = delegate;
         }
 
@@ -565,6 +577,11 @@
         public void onTimeShiftStatusChanged(String inputId, int status) {
             mDelegate.onTimeShiftStatusChanged(inputId, status);
         }
+
+        @Override
+        public void onSignalStrength(String inputId, int value) {
+            mDelegate.onSignalStrength(inputId, value);
+        }
     }
 
     /** Called when the {@link TvView} channel is changed. */
diff --git a/src/com/android/tv/MainActivity.java b/src/com/android/tv/MainActivity.java
index 94a86cc..b4cf71d 100644
--- a/src/com/android/tv/MainActivity.java
+++ b/src/com/android/tv/MainActivity.java
@@ -22,7 +22,6 @@
 import android.content.ActivityNotFoundException;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
-import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.Context;
 import android.content.Intent;
@@ -49,6 +48,7 @@
 import android.support.annotation.IntDef;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
@@ -65,16 +65,22 @@
 import android.view.accessibility.AccessibilityManager;
 import android.widget.FrameLayout;
 import android.widget.Toast;
+import com.android.tv.MainActivity.MySingletons;
 import com.android.tv.analytics.SendChannelStatusRunnable;
 import com.android.tv.analytics.SendConfigInfoRunnable;
 import com.android.tv.analytics.Tracker;
+import com.android.tv.audio.AudioManagerHelper;
+import com.android.tv.audiotvservice.AudioOnlyTvServiceUtil;
 import com.android.tv.common.BuildConfig;
+import com.android.tv.common.CommonConstants;
 import com.android.tv.common.CommonPreferences;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.TvContentRatingCache;
 import com.android.tv.common.WeakHandler;
+import com.android.tv.common.compat.TvInputInfoCompat;
 import com.android.tv.common.feature.CommonFeatures;
 import com.android.tv.common.memory.MemoryManageable;
+import com.android.tv.common.singletons.HasSingletons;
 import com.android.tv.common.ui.setup.OnActionClickListener;
 import com.android.tv.common.util.CommonUtils;
 import com.android.tv.common.util.ContentUriUtils;
@@ -99,17 +105,19 @@
 import com.android.tv.dvr.recorder.ConflictChecker;
 import com.android.tv.dvr.ui.DvrStopRecordingFragment;
 import com.android.tv.dvr.ui.DvrUiHelper;
+import com.android.tv.features.TvFeatures;
 import com.android.tv.menu.Menu;
 import com.android.tv.onboarding.OnboardingActivity;
 import com.android.tv.parental.ContentRatingsManager;
 import com.android.tv.parental.ParentalControlSettings;
-import com.android.tv.perf.EventNames;
-import com.android.tv.perf.PerformanceMonitor;
-import com.android.tv.perf.TimerEvent;
+import com.android.tv.perf.PerformanceMonitorManagerFactory;
+import com.android.tv.receiver.AudioCapabilitiesReceiver;
 import com.android.tv.recommendation.ChannelPreviewUpdater;
 import com.android.tv.recommendation.NotificationService;
 import com.android.tv.search.ProgramGuideSearchFragment;
+import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
 import com.android.tv.ui.ChannelBannerView;
+import com.android.tv.ui.DetailsActivity;
 import com.android.tv.ui.InputBannerView;
 import com.android.tv.ui.KeypadChannelSwitchView;
 import com.android.tv.ui.SelectInputView;
@@ -128,6 +136,7 @@
 import com.android.tv.ui.sidepanel.SideFragment;
 import com.android.tv.ui.sidepanel.parentalcontrols.ParentalControlsFragment;
 import com.android.tv.util.AsyncDbTask;
+import com.android.tv.util.AsyncDbTask.DbExecutor;
 import com.android.tv.util.CaptionSettings;
 import com.android.tv.util.OnboardingUtils;
 import com.android.tv.util.RecurringRunner;
@@ -140,6 +149,10 @@
 import com.android.tv.util.account.AccountHelper;
 import com.android.tv.util.images.ImageCache;
 
+import com.google.common.base.Optional;
+import dagger.android.AndroidInjection;
+import dagger.android.ContributesAndroidInjector;
+import com.android.tv.common.flags.BackendKnobsFlags;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayDeque;
@@ -150,11 +163,21 @@
 import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
+import javax.inject.Inject;
+import javax.inject.Provider;
 
 /** The main activity for the Live TV app. */
-public class MainActivity extends Activity implements OnActionClickListener, OnPinCheckedListener {
+public class MainActivity extends Activity
+        implements OnActionClickListener,
+                OnPinCheckedListener,
+                ChannelChanger,
+                HasSingletons<MySingletons> {
     private static final String TAG = "MainActivity";
     private static final boolean DEBUG = false;
+    private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
+
+    /** Singletons needed for this class. */
+    public interface MySingletons extends ChannelBannerView.MySingletons {}
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({
@@ -175,6 +198,8 @@
     private static final float FRAME_RATE_FOR_FILM = 23.976f;
     private static final float FRAME_RATE_EPSILON = 0.1f;
 
+// AOSP_Comment_Out     private static final String PLUTO_TV_PACKAGE_NAME = "tv.pluto.android";
+
     private static final int PERMISSIONS_REQUEST_READ_TV_LISTINGS = 1;
     private static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS";
 
@@ -232,10 +257,17 @@
     private static final int UNDEFINED_TRACK_INDEX = -1;
     private static final long START_UP_TIMER_RESET_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(3);
 
+    {
+        PerformanceMonitorManagerFactory.create().getStartupMeasure().onActivityInit();
+    }
+
+    private final MySingletonsImpl mMySingletons = new MySingletonsImpl();
+    @Inject @DbExecutor Executor mDbExecutor;
+
     private AccessibilityManager mAccessibilityManager;
-    private ChannelDataManager mChannelDataManager;
-    private ProgramDataManager mProgramDataManager;
-    private TvInputManagerHelper mTvInputManagerHelper;
+    @Inject ChannelDataManager mChannelDataManager;
+    @Inject ProgramDataManager mProgramDataManager;
+    @Inject TvInputManagerHelper mTvInputManagerHelper;
     private ChannelTuner mChannelTuner;
     private final TvOptionsManager mTvOptionsManager = new TvOptionsManager(this);
     private TvViewUiManager mTvViewUiManager;
@@ -245,10 +277,12 @@
     private final DurationTimer mTuneDurationTimer = new DurationTimer();
     private DvrManager mDvrManager;
     private ConflictChecker mDvrConflictChecker;
-    private SetupUtils mSetupUtils;
+    @Inject BackendKnobsFlags mBackendKnobs;
+    @Inject SetupUtils mSetupUtils;
+    @Inject Optional<BuiltInTunerManager> mOptionalBuiltInTunerManager;
 
+    @VisibleForTesting protected TunableTvView mTvView;
     private View mContentView;
-    private TunableTvView mTvView;
     private Bundle mTuneParams;
     @Nullable private Uri mInitChannelUri;
     @Nullable private String mParentInputIdWhenScreenOff;
@@ -274,9 +308,7 @@
     private boolean mNeedShowBackKeyGuide;
     private boolean mVisibleBehind;
     private boolean mShowNewSourcesFragment = true;
-    private String mTunerInputId;
     private boolean mOtherActivityLaunched;
-    private PerformanceMonitor mPerformanceMonitor;
 
     private boolean mIsInPIPMode;
     private boolean mIsFilmModeSet;
@@ -304,6 +336,8 @@
     private RecurringRunner mSendConfigInfoRecurringRunner;
     private RecurringRunner mChannelStatusRecurringRunner;
 
+    private String mLastInputIdFromIntent;
+
     private final Handler mHandler = new MainActivityHandler(this);
     private final Set<OnActionClickListener> mOnActionClickListeners = new ArraySet<>();
 
@@ -399,28 +433,27 @@
                 public void onChannelChanged(Channel previousChannel, Channel currentChannel) {}
             };
 
-    private final Runnable mRestoreMainViewRunnable =
-            new Runnable() {
-                @Override
-                public void run() {
-                    restoreMainTvView();
-                }
-            };
+    private final Runnable mRestoreMainViewRunnable = this::restoreMainTvView;
     private ProgramGuideSearchFragment mSearchFragment;
 
     private final TvInputCallback mTvInputCallback =
             new TvInputCallback() {
                 @Override
                 public void onInputAdded(String inputId) {
-                    if (TvFeatures.TUNER.isEnabled(MainActivity.this)
-                            && mTunerInputId.equals(inputId)
+                    if (mOptionalBuiltInTunerManager.isPresent()
                             && CommonPreferences.shouldShowSetupActivity(MainActivity.this)) {
-                        Intent intent =
-                                TvSingletons.getSingletons(MainActivity.this)
-                                        .getTunerSetupIntent(MainActivity.this);
-                        startActivity(intent);
-                        CommonPreferences.setShouldShowSetupActivity(MainActivity.this, false);
-                        mSetupUtils.markAsKnownInput(mTunerInputId);
+                        BuiltInTunerManager builtInTunerManager =
+                                mOptionalBuiltInTunerManager.get();
+                        String tunerInputId = builtInTunerManager.getEmbeddedTunerInputId();
+                        if (tunerInputId.equals(inputId)) {
+                            Intent intent =
+                                    builtInTunerManager
+                                            .getTunerInputController()
+                                            .createSetupIntent(MainActivity.this);
+                            startActivity(intent);
+                            CommonPreferences.setShouldShowSetupActivity(MainActivity.this, false);
+                            mSetupUtils.markAsKnownInput(tunerInputId);
+                        }
                     }
                 }
             };
@@ -435,12 +468,16 @@
     }
 
     @Override
+    public MySingletons singletons() {
+        return mMySingletons;
+    }
+
+    @Override
     protected void onCreate(Bundle savedInstanceState) {
+        AndroidInjection.inject(this);
         mAccessibilityManager =
                 (AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE);
         TvSingletons tvSingletons = TvSingletons.getSingletons(this);
-        mPerformanceMonitor = tvSingletons.getPerformanceMonitor();
-        TimerEvent timer = mPerformanceMonitor.startTimer();
         DurationTimer startUpDebugTimer = Debug.getTimer(Debug.TAG_START_UP_TIMER);
         if (!startUpDebugTimer.isStarted()
                 || startUpDebugTimer.getDuration() > START_UP_TIMER_RESET_THRESHOLD_MS) {
@@ -454,16 +491,13 @@
         }
         Starter.start(this);
         super.onCreate(savedInstanceState);
-        if (!tvSingletons.getTvInputManagerHelper().hasTvInputManager()) {
+        if (!mTvInputManagerHelper.hasTvInputManager()) {
             Log.wtf(TAG, "Stopping because device does not have a TvInputManager");
             finishAndRemoveTask();
             return;
         }
-        mPerformanceMonitor = tvSingletons.getPerformanceMonitor();
-        mSetupUtils = tvSingletons.getSetupUtils();
 
-        TvApplication tvApplication = (TvApplication) getApplication();
-        mChannelDataManager = tvApplication.getChannelDataManager();
+        TvSingletons tvApplication = (TvSingletons) getApplication();
         // In API 23, TvContract.isChannelUriForPassthroughInput is hidden.
         boolean isPassthroughInput =
                 TvContract.isChannelUriForPassthroughInput(getIntent().getData());
@@ -480,17 +514,12 @@
             return;
         }
         setContentView(R.layout.activity_tv);
-        mProgramDataManager = tvApplication.getProgramDataManager();
-        mTvInputManagerHelper = tvApplication.getTvInputManagerHelper();
         mTvView = (TunableTvView) findViewById(R.id.main_tunable_tv_view);
         mTvView.initialize(mProgramDataManager, mTvInputManagerHelper);
         mTvView.setOnUnhandledInputEventListener(
                 new OnUnhandledInputEventListener() {
                     @Override
                     public boolean onUnhandledInputEvent(InputEvent event) {
-                        if (DEBUG) {
-                            Log.d(TAG, "onUnhandledInputEvent " + event);
-                        }
                         if (isKeyEventBlocked()) {
                             return true;
                         }
@@ -511,7 +540,7 @@
                         return false;
                     }
                 });
-        mTvView.setOnTalkBackDpadKeyListener(keycode -> handleUpDownKeys(keycode, null));
+        mTvView.setBlockedInfoOnClickListener(v -> showPinDialogFragment());
         long channelId = Utils.getLastWatchedChannelId(this);
         String inputId = Utils.getLastWatchedTunerInputId(this);
         if (!isPassthroughInput
@@ -525,10 +554,9 @@
             Toast.makeText(this, "Using Strict Mode for eng builds", Toast.LENGTH_SHORT).show();
         }
         mTracker = tvApplication.getTracker();
-        if (TvFeatures.TUNER.isEnabled(this)) {
+        if (mOptionalBuiltInTunerManager.isPresent()) {
             mTvInputManagerHelper.addCallback(mTvInputCallback);
         }
-        mTunerInputId = tvSingletons.getEmbeddedTunerInputId();
         mProgramDataManager.addOnCurrentProgramUpdatedListener(
                 Channel.INVALID_ID, mOnCurrentProgramUpdatedListener);
         mProgramDataManager.setPrefetchEnabled(true);
@@ -657,6 +685,8 @@
         mAccessibilityManager.addAccessibilityStateChangeListener(mOverlayManager);
 
         mAudioManagerHelper = new AudioManagerHelper(this, mTvView);
+        mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(this, null);
+        mAudioCapabilitiesReceiver.register();
         Intent nowPlayingIntent = new Intent(this, MainActivity.class);
         PendingIntent pendingIntent =
                 PendingIntent.getActivity(this, REQUEST_CODE_NOW_PLAYING, nowPlayingIntent, 0);
@@ -687,7 +717,6 @@
         }
         initForTest();
         Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onCreate end");
-        mPerformanceMonitor.stopTimer(timer, EventNames.MAIN_ACTIVITY_ONCREATE);
     }
 
     private void startOnboardingActivity() {
@@ -778,7 +807,6 @@
 
     @Override
     protected void onStart() {
-        TimerEvent timer = mPerformanceMonitor.startTimer();
         if (DEBUG) {
             Log.d(TAG, "onStart()");
         }
@@ -796,15 +824,17 @@
             notificationIntent.setAction(NotificationService.ACTION_SHOW_RECOMMENDATION);
             startService(notificationIntent);
         }
-        TvSingletons singletons = TvSingletons.getSingletons(this);
-        singletons.getTunerInputController().executeNetworkTunerDiscoveryAsyncTask(this);
-        singletons.getEpgFetcher().fetchImmediatelyIfNeeded();
-        mPerformanceMonitor.stopTimer(timer, EventNames.MAIN_ACTIVITY_ONSTART);
+        if (mOptionalBuiltInTunerManager.isPresent()) {
+            mOptionalBuiltInTunerManager
+                    .get()
+                    .getTunerInputController()
+                    .executeNetworkTunerDiscoveryAsyncTask(this);
+        }
+        TvSingletons.getSingletons(this).getEpgFetcher().fetchImmediatelyIfNeeded();
     }
 
     @Override
     protected void onResume() {
-        TimerEvent timer = mPerformanceMonitor.startTimer();
         Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onResume start");
         if (DEBUG) Log.d(TAG, "onResume()");
         super.onResume();
@@ -836,13 +866,9 @@
                         getApplicationContext(), TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE)
                 && !failedScheduledRecordingInfoSet.isEmpty()) {
             runAfterAttachedToWindow(
-                    new Runnable() {
-                        @Override
-                        public void run() {
+                    () ->
                             DvrUiHelper.showDvrInsufficientSpaceErrorDialog(
-                                    MainActivity.this, failedScheduledRecordingInfoSet);
-                        }
-                    });
+                                    MainActivity.this, failedScheduledRecordingInfoSet));
         }
 
         if (mChannelTuner.areAllChannelsLoaded()) {
@@ -861,32 +887,23 @@
             // This will delay the start of the animation until after the Live Channel app is
             // shown. Without this the animation is completed before it is actually visible on
             // the screen.
-            mHandler.post(
-                    new Runnable() {
-                        @Override
-                        public void run() {
-                            mOverlayManager.showProgramGuide();
-                        }
-                    });
+            mHandler.post(() -> mOverlayManager.showProgramGuide());
         } else if (mShowSelectInputView) {
             mShowSelectInputView = false;
             // mShowSelectInputView is true when the activity is started/resumed because the
             // TV_INPUT button was pressed in a different app.  This will delay the start of
             // the animation until after the Live Channel app is shown. Without this the
             // animation is completed before it is actually visible on the screen.
-            mHandler.post(
-                    new Runnable() {
-                        @Override
-                        public void run() {
-                            mOverlayManager.showSelectInputView();
-                        }
-                    });
+            mHandler.post(() -> mOverlayManager.showSelectInputView());
         }
         if (mDvrConflictChecker != null) {
             mDvrConflictChecker.start();
         }
+        if (CommonFeatures.ENABLE_TV_SERVICE.isEnabled(this) && isAudioOnlyInput()) {
+            // TODO(b/110969180): figure out when to call AudioOnlyTvServiceUtil.stopAudioOnlyInput
+            AudioOnlyTvServiceUtil.startAudioOnlyInput(this, mLastInputIdFromIntent);
+        }
         Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onResume end");
-        mPerformanceMonitor.stopTimer(timer, EventNames.MAIN_ACTIVITY_ONRESUME);
     }
 
     @Override
@@ -913,7 +930,6 @@
         } else {
             mTracker.sendScreenView(SCREEN_BEHIND_NAME);
         }
-        TvSingletons.getSingletons(this).getExperimentLoader().asyncRefreshExperiments(this);
         super.onPause();
     }
 
@@ -1068,6 +1084,9 @@
                 markCurrentChannelDuringScreenOff();
             }
         }
+        if (mChannelTuner.isCurrentChannelPassthrough()) {
+            mInitChannelUri = mChannelTuner.getCurrentChannelUri();
+        }
         mActivityStarted = false;
         stopAll(false);
         unregisterReceiver(mBroadcastReceiver);
@@ -1299,19 +1318,15 @@
         if (!Objects.equals(mTvView.getCurrentChannel(), returnChannel)) {
             final Channel channel = returnChannel;
             Runnable tuneAction =
-                    new Runnable() {
-                        @Override
-                        public void run() {
-                            tuneToChannel(channel);
-                            if (mChannelBeforeShrunkenTvView == null
-                                    || !mChannelBeforeShrunkenTvView.equals(channel)) {
-                                Utils.setLastWatchedChannel(MainActivity.this, channel);
-                            }
-                            mIsCompletingShrunkenTvView = false;
-                            mIsCurrentChannelUnblockedByUser =
-                                    mWasChannelUnblockedBeforeShrunkenByUser;
-                            mTvView.setBlockScreenType(getDesiredBlockScreenType());
+                    () -> {
+                        tuneToChannel(channel);
+                        if (mChannelBeforeShrunkenTvView == null
+                                || !mChannelBeforeShrunkenTvView.equals(channel)) {
+                            Utils.setLastWatchedChannel(MainActivity.this, channel);
                         }
+                        mIsCompletingShrunkenTvView = false;
+                        mIsCurrentChannelUnblockedByUser = mWasChannelUnblockedBeforeShrunkenByUser;
+                        mTvView.setBlockScreenType(getDesiredBlockScreenType());
                     };
             mTvViewUiManager.fadeOutTvView(tuneAction);
             // Will automatically fade-in when video becomes available.
@@ -1423,17 +1438,12 @@
 
     /** Notifies the key input focus is changed to the TV view. */
     public void updateKeyInputFocus() {
-        mHandler.post(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        mTvView.setBlockScreenType(getDesiredBlockScreenType());
-                    }
-                });
+        mHandler.post(() -> mTvView.setBlockScreenType(getDesiredBlockScreenType()));
     }
 
     // It should be called before onResume.
     private boolean handleIntent(Intent intent) {
+        mLastInputIdFromIntent = getInputId(intent);
         // Reset the closed caption settings when the activity is 1)created or 2) restarted.
         // And do not reset while TvView is playing.
         if (!mTvView.isPlaying()) {
@@ -1455,13 +1465,7 @@
         }
 
         if (TvInputManager.ACTION_SETUP_INPUTS.equals(intent.getAction())) {
-            runAfterAttachedToWindow(
-                    new Runnable() {
-                        @Override
-                        public void run() {
-                            mOverlayManager.showSetupFragment();
-                        }
-                    });
+            runAfterAttachedToWindow(() -> mOverlayManager.showSetupFragment());
         } else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
             Uri uri = intent.getData();
             if (Utils.isProgramsUri(uri)) {
@@ -1497,8 +1501,7 @@
             long channelIdFromIntent = ContentUriUtils.safeParseId(mInitChannelUri);
             if (programUriFromIntent != null && channelIdFromIntent != Channel.INVALID_ID) {
                 new AsyncQueryProgramTask(
-                                TvSingletons.getSingletons(this).getDbExecutor(),
-                                getContentResolver(),
+                                mDbExecutor,
                                 programUriFromIntent,
                                 Program.PROJECTION,
                                 null,
@@ -1565,14 +1568,13 @@
 
         public AsyncQueryProgramTask(
                 Executor executor,
-                ContentResolver contentResolver,
                 Uri uri,
                 String[] projection,
                 String selection,
                 String[] selectionArgs,
                 String orderBy,
                 long channelId) {
-            super(executor, contentResolver, uri, projection, selection, selectionArgs, orderBy);
+            super(executor, MainActivity.this, uri, projection, selection, selectionArgs, orderBy);
             mChannelIdFromIntent = channelId;
         }
 
@@ -1593,26 +1595,12 @@
             }
             Channel channel = mChannelDataManager.getChannel(mChannelIdFromIntent);
             if (channel != null) {
-                ScheduledRecording scheduledRecording =
-                        TvSingletons.getSingletons(MainActivity.this)
-                                .getDvrDataManager()
-                                .getScheduledRecordingForProgramId(program.getId());
-                DvrUiHelper.checkStorageStatusAndShowErrorMessage(
-                        MainActivity.this,
-                        channel.getInputId(),
-                        new Runnable() {
-                            @Override
-                            public void run() {
-                                if (CommonFeatures.DVR.isEnabled(MainActivity.this)
-                                        && scheduledRecording == null
-                                        && mDvrManager.isProgramRecordable(program)) {
-                                    DvrUiHelper.requestRecordingFutureProgram(
-                                            MainActivity.this, program, false);
-                                } else {
-                                    DvrUiHelper.showProgramInfoDialog(MainActivity.this, program);
-                                }
-                            }
-                        });
+                Intent intent = new Intent(MainActivity.this, DetailsActivity.class);
+                intent.putExtra(DetailsActivity.CHANNEL_ID, mChannelIdFromIntent);
+                intent.putExtra(DetailsActivity.DETAILS_VIEW_TYPE, DetailsActivity.PROGRAM_VIEW);
+                intent.putExtra(DetailsActivity.PROGRAM, program);
+                intent.putExtra(DetailsActivity.INPUT_ID, channel.getInputId());
+                startActivity(intent);
             }
         }
     }
@@ -1671,6 +1659,11 @@
             return;
         }
         mTunePending = false;
+        if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(this)) {
+            mTvView.resetChannelSignalStrength();
+            mOverlayManager.updateChannelBannerAndShowIfNeeded(
+                    TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_UPDATE_SIGNAL_STRENGTH);
+        }
         final Channel channel = mChannelTuner.getCurrentChannel();
         SoftPreconditions.checkState(channel != null);
         if (channel == null) {
@@ -1717,18 +1710,14 @@
                     && mSetupUtils.hasUnrecognizedInput(mTvInputManagerHelper)) {
                 // Show new channel sources fragment.
                 runAfterAttachedToWindow(
-                        new Runnable() {
-                            @Override
-                            public void run() {
+                        () ->
                                 mOverlayManager.runAfterOverlaysAreClosed(
                                         new Runnable() {
                                             @Override
                                             public void run() {
                                                 mOverlayManager.showNewSourcesFragment();
                                             }
-                                        });
-                            }
-                        });
+                                        }));
             }
             mSetupUtils.onTuned();
             if (mTuneParams != null) {
@@ -1799,12 +1788,9 @@
     // should be closed when the activity is paused.
     private void runAfterAttachedToWindow(final Runnable runnable) {
         final Runnable runOnlyIfActivityIsResumed =
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        if (mActivityResumed) {
-                            runnable.run();
-                        }
+                () -> {
+                    if (mActivityResumed) {
+                        runnable.run();
                     }
                 };
         if (mContentView.isAttachedToWindow()) {
@@ -1918,25 +1904,36 @@
         window.setAttributes(layoutParams);
     }
 
-    private void applyMultiAudio() {
+    @VisibleForTesting
+    protected void applyMultiAudio(String trackId) {
         List<TvTrackInfo> tracks = getTracks(TvTrackInfo.TYPE_AUDIO);
         if (tracks == null) {
             mTvOptionsManager.onMultiAudioChanged(null);
             return;
         }
 
-        String id = TvSettings.getMultiAudioId(this);
-        String language = TvSettings.getMultiAudioLanguage(this);
-        int channelCount = TvSettings.getMultiAudioChannelCount(this);
-        TvTrackInfo bestTrack =
-                TvTrackInfoUtils.getBestTrackInfo(tracks, id, language, channelCount);
+        TvTrackInfo bestTrack = null;
+        if (trackId != null) {
+            for (TvTrackInfo track : tracks) {
+                if (trackId.equals(track.getId())) {
+                    bestTrack = track;
+                    break;
+                }
+            }
+        }
+        if (bestTrack == null) {
+            String id = TvSettings.getMultiAudioId(this);
+            String language = TvSettings.getMultiAudioLanguage(this);
+            int channelCount = TvSettings.getMultiAudioChannelCount(this);
+            bestTrack = TvTrackInfoUtils.getBestTrackInfo(tracks, id, language, channelCount);
+        }
         if (bestTrack != null) {
             String selectedTrack = getSelectedTrack(TvTrackInfo.TYPE_AUDIO);
             if (!bestTrack.getId().equals(selectedTrack)) {
                 selectTrack(TvTrackInfo.TYPE_AUDIO, bestTrack, UNDEFINED_TRACK_INDEX);
             } else {
                 mTvOptionsManager.onMultiAudioChanged(
-                        Utils.getMultiAudioString(this, bestTrack, false));
+                        TvTrackInfoUtils.getMultiAudioString(this, bestTrack, false));
             }
             return;
         }
@@ -2056,8 +2053,8 @@
         if (mMediaSessionWrapper != null) {
             mMediaSessionWrapper.release();
         }
-        if (mAudioManagerHelper != null) {
-            mAudioManagerHelper.release();
+        if (mAudioCapabilitiesReceiver != null) {
+            mAudioCapabilitiesReceiver.unregister();
         }
         mHandler.removeCallbacksAndMessages(null);
         application.getMainActivityWrapper().onMainActivityDestroyed(this);
@@ -2071,7 +2068,7 @@
         }
         if (mTvInputManagerHelper != null) {
             mTvInputManagerHelper.clearTvInputLabels();
-            if (TvFeatures.TUNER.isEnabled(this)) {
+            if (mOptionalBuiltInTunerManager.isPresent()) {
                 mTvInputManagerHelper.removeCallback(mTvInputCallback);
             }
         }
@@ -2100,51 +2097,59 @@
         if (!mChannelTuner.areAllChannelsLoaded()) {
             return false;
         }
-        if (handleUpDownKeys(keyCode, event)) {
-            return true;
-        }
-        return super.onKeyDown(keyCode, event);
-    }
-
-    private boolean handleUpDownKeys(int keyCode, @Nullable KeyEvent event) {
         if (!mChannelTuner.isCurrentChannelPassthrough()) {
             switch (keyCode) {
                 case KeyEvent.KEYCODE_CHANNEL_UP:
                 case KeyEvent.KEYCODE_DPAD_UP:
-                    if ((event == null || event.getRepeatCount() == 0)
+                    if (event.getRepeatCount() == 0
                             && mChannelTuner.getBrowsableChannelCount() > 0) {
-                        // message sending should be done before moving channel, because we use the
-                        // existence of message to decide if users are switching channel.
-                        if (event != null) {
-                            mHandler.sendMessageDelayed(
-                                    mHandler.obtainMessage(
-                                            MSG_CHANNEL_UP_PRESSED, System.currentTimeMillis()),
-                                    CHANNEL_CHANGE_INITIAL_DELAY_MILLIS);
-                        }
-                        moveToAdjacentChannel(true, false);
-                        mTracker.sendChannelUp();
+
+                        channelUpPressed();
                     }
                     return true;
                 case KeyEvent.KEYCODE_CHANNEL_DOWN:
                 case KeyEvent.KEYCODE_DPAD_DOWN:
-                    if ((event == null || event.getRepeatCount() == 0)
+                    if (event.getRepeatCount() == 0
                             && mChannelTuner.getBrowsableChannelCount() > 0) {
-                        // message sending should be done before moving channel, because we use the
-                        // existence of message to decide if users are switching channel.
-                        if (event != null) {
-                            mHandler.sendMessageDelayed(
-                                    mHandler.obtainMessage(
-                                            MSG_CHANNEL_DOWN_PRESSED, System.currentTimeMillis()),
-                                    CHANNEL_CHANGE_INITIAL_DELAY_MILLIS);
-                        }
-                        moveToAdjacentChannel(false, false);
-                        mTracker.sendChannelDown();
+                        channelDownPressed();
                     }
                     return true;
                 default: // fall out
             }
         }
-        return false;
+        return super.onKeyDown(keyCode, event);
+    }
+
+    @Override
+    public void channelDown() {
+        channelDownPressed();
+        finishChannelChangeIfNeeded();
+    }
+
+    private void channelDownPressed() {
+        // message sending should be done before moving channel, because we use the
+        // existence of message to decide if users are switching channel.
+        mHandler.sendMessageDelayed(
+                mHandler.obtainMessage(MSG_CHANNEL_DOWN_PRESSED, System.currentTimeMillis()),
+                CHANNEL_CHANGE_INITIAL_DELAY_MILLIS);
+        moveToAdjacentChannel(false, false);
+        mTracker.sendChannelDown();
+    }
+
+    @Override
+    public void channelUp() {
+        channelUpPressed();
+        finishChannelChangeIfNeeded();
+    }
+
+    private void channelUpPressed() {
+        // message sending should be done before moving channel, because we use the
+        // existence of message to decide if users are switching channel.
+        mHandler.sendMessageDelayed(
+                mHandler.obtainMessage(MSG_CHANNEL_UP_PRESSED, System.currentTimeMillis()),
+                CHANNEL_CHANGE_INITIAL_DELAY_MILLIS);
+        moveToAdjacentChannel(true, false);
+        mTracker.sendChannelUp();
     }
 
     @Override
@@ -2228,24 +2233,7 @@
                                 this, mChannelTuner.getCurrentChannel());
                         return true;
                     }
-                    if (!PermissionUtils.hasModifyParentalControls(this)) {
-                        return true;
-                    }
-                    PinDialogFragment dialog = null;
-                    if (mTvView.isScreenBlocked()) {
-                        dialog =
-                                PinDialogFragment.create(
-                                        PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_CHANNEL);
-                    } else if (mTvView.isContentBlocked()) {
-                        dialog =
-                                PinDialogFragment.create(
-                                        PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM,
-                                        mTvView.getBlockedContentRating().flattenToString());
-                    }
-                    if (dialog != null) {
-                        mOverlayManager.showDialogFragment(
-                                PinDialogFragment.DIALOG_TAG, dialog, false);
-                    }
+                    showPinDialogFragment();
                     return true;
                 case KeyEvent.KEYCODE_WINDOW:
                     enterPictureInPictureMode();
@@ -2315,16 +2303,12 @@
                                 DvrUiHelper.checkStorageStatusAndShowErrorMessage(
                                         this,
                                         currentChannel.getInputId(),
-                                        new Runnable() {
-                                            @Override
-                                            public void run() {
+                                        () ->
                                                 DvrUiHelper.requestRecordingCurrentProgram(
                                                         MainActivity.this,
                                                         currentChannel,
                                                         program,
-                                                        false);
-                                            }
-                                        });
+                                                        false));
                             }
                         } else {
                             DvrUiHelper.showStopRecordingDialog(
@@ -2391,6 +2375,24 @@
         return super.onKeyUp(keyCode, event);
     }
 
+    private void showPinDialogFragment() {
+        if (!PermissionUtils.hasModifyParentalControls(this)) {
+            return;
+        }
+        PinDialogFragment dialog = null;
+        if (mTvView.isScreenBlocked()) {
+            dialog = PinDialogFragment.create(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_CHANNEL);
+        } else if (mTvView.isContentBlocked()) {
+            dialog =
+                    PinDialogFragment.create(
+                            PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM,
+                            mTvView.getBlockedContentRating().flattenToString());
+        }
+        if (dialog != null) {
+            mOverlayManager.showDialogFragment(PinDialogFragment.DIALOG_TAG, dialog, false);
+        }
+    }
+
     @Override
     public boolean onKeyLongPress(int keyCode, KeyEvent event) {
         if (SystemProperties.LOG_KEYEVENT.getValue()) Log.d(TAG, "onKeyLongPress(" + event);
@@ -2423,13 +2425,7 @@
         mIsInPIPMode = true;
         if (mOverlayManager.isOverlayOpened()) {
             mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION);
-            mHandler.post(
-                    new Runnable() {
-                        @Override
-                        public void run() {
-                            MainActivity.super.enterPictureInPictureMode();
-                        }
-                    });
+            mHandler.post(MainActivity.super::enterPictureInPictureMode);
         } else {
             MainActivity.super.enterPictureInPictureMode();
         }
@@ -2586,7 +2582,9 @@
         mTvView.selectTrack(type, track == null ? null : track.getId());
         if (type == TvTrackInfo.TYPE_AUDIO) {
             mTvOptionsManager.onMultiAudioChanged(
-                    track == null ? null : Utils.getMultiAudioString(this, track, false));
+                    track == null
+                            ? null
+                            : TvTrackInfoUtils.getMultiAudioString(this, track, false));
         } else if (type == TvTrackInfo.TYPE_SUBTITLE) {
             mTvOptionsManager.onClosedCaptionsChanged(track, trackIndex);
         }
@@ -2594,7 +2592,7 @@
 
     public void selectAudioTrack(String trackId) {
         saveMultiAudioSetting(trackId);
-        applyMultiAudio();
+        applyMultiAudio(trackId);
     }
 
     private void saveMultiAudioSetting(String trackId) {
@@ -2657,6 +2655,13 @@
             case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY:
             case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
                 return;
+            case CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED:
+                Toast.makeText(
+                                this,
+                                R.string.msg_channel_unavailable_not_connected,
+                                Toast.LENGTH_SHORT)
+                        .show();
+                break;
             case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
             default:
                 Toast.makeText(this, R.string.msg_channel_unavailable_unknown, Toast.LENGTH_SHORT)
@@ -2725,14 +2730,11 @@
         mLazyInitialized = true;
         // Running initialization.
         mHandler.postDelayed(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        if (mActivityStarted) {
-                            initAnimations();
-                            initSideFragments();
-                            initMenuItemViews();
-                        }
+                () -> {
+                    if (mActivityStarted) {
+                        initAnimations();
+                        initSideFragments();
+                        initMenuItemViews();
                     }
                 },
                 LAZY_INITIALIZATION_DELAY);
@@ -2751,6 +2753,23 @@
         mOverlayManager.getMenu().preloadItemViews();
     }
 
+    private boolean isAudioOnlyInput() {
+        if (mLastInputIdFromIntent == null) {
+            return false;
+        }
+        TvInputInfoCompat inputInfo =
+                mTvInputManagerHelper.getTvInputInfoCompat(mLastInputIdFromIntent);
+        return inputInfo != null && inputInfo.isAudioOnly();
+    }
+
+    @Nullable
+    private String getInputId(Intent intent) {
+        Uri uri = intent.getData();
+        return TvContract.isChannelUriForPassthroughInput(uri)
+                ? uri.getPathSegments().get(1)
+                : null;
+    }
+
     @Override
     public void onTrimMemory(int level) {
         super.onTrimMemory(level);
@@ -2793,15 +2812,22 @@
         }
     }
 
-    private class MyOnTuneListener implements OnTuneListener {
+    /** {@link OnTuneListener} implementation */
+    @VisibleForTesting
+    protected class MyOnTuneListener implements OnTuneListener {
         boolean mUnlockAllowedRatingBeforeShrunken = true;
         boolean mWasUnderShrunkenTvView;
         Channel mChannel;
 
-        private void onTune(Channel channel, boolean wasUnderShrukenTvView) {
+        private void onTune(Channel channel, boolean wasUnderShrunkenTvView) {
             Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.MyOnTuneListener.onTune");
             mChannel = channel;
-            mWasUnderShrunkenTvView = wasUnderShrukenTvView;
+            mWasUnderShrunkenTvView = wasUnderShrunkenTvView;
+
+            if (mBackendKnobs.enablePartialProgramFetch()) {
+                // Fetch complete projection of tuned channel.
+                mProgramDataManager.prefetchChannel(channel.getId());
+            }
         }
 
         @Override
@@ -2824,7 +2850,7 @@
         }
 
         @Override
-        public void onStreamInfoChanged(StreamInfo info) {
+        public void onStreamInfoChanged(StreamInfo info, boolean allowAutoSelectionOfTrack) {
             if (info.isVideoAvailable() && mTuneDurationTimer.isRunning()) {
                 mTracker.sendChannelTuneTime(info.getCurrentChannel(), mTuneDurationTimer.reset());
             }
@@ -2834,7 +2860,8 @@
             }
             applyDisplayRefreshRate(info.getVideoFrameRate());
             mTvViewUiManager.updateTvAspectRatio();
-            applyMultiAudio();
+            applyMultiAudio(
+                    allowAutoSelectionOfTrack ? null : getSelectedTrack(TvTrackInfo.TYPE_AUDIO));
             applyClosedCaption();
             mOverlayManager.getMenu().onStreamInfoChanged();
             if (mTvView.isVideoAvailable()) {
@@ -2861,6 +2888,12 @@
                                 + channel);
                 return;
             }
+            /* Begin_AOSP_Comment_Out
+            if (PLUTO_TV_PACKAGE_NAME.equals(currentChannel.getPackageName())) {
+                // Do nothing for the Pluto TV input because it misuses this API. b/22720711.
+                return;
+            }
+            End_AOSP_Comment_Out */
             if (isChannelChangeKeyDownReceived()) {
                 // Ignore this message if the user is changing the channel.
                 return;
@@ -2883,7 +2916,7 @@
             // before.
             if (mWasUnderShrunkenTvView
                     && mUnlockAllowedRatingBeforeShrunken
-                    && mChannelBeforeShrunkenTvView.equals(mChannel)
+                    && Objects.equals(mChannelBeforeShrunkenTvView, mChannel)
                     && rating.equals(mAllowedRatingBeforeShrunken)) {
                 mUnlockAllowedRatingBeforeShrunken = isUnderShrunkenTvView();
                 mTvView.unblockContent(rating);
@@ -2901,5 +2934,53 @@
             mOverlayManager.setBlockingContentRating(null);
             mMediaSessionWrapper.update(false, getCurrentChannel(), getCurrentProgram());
         }
+
+        @Override
+        public void onChannelSignalStrength() {
+            if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(getApplicationContext())) {
+                mOverlayManager.updateChannelBannerAndShowIfNeeded(
+                        TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_UPDATE_SIGNAL_STRENGTH);
+            }
+        }
+    }
+
+    private class MySingletonsImpl implements MySingletons {
+
+        @Override
+        public Provider<Channel> getCurrentChannelProvider() {
+            return MainActivity.this::getCurrentChannel;
+        }
+
+        @Override
+        public Provider<Program> getCurrentProgramProvider() {
+            return MainActivity.this::getCurrentProgram;
+        }
+
+        @Override
+        public Provider<TvOverlayManager> getOverlayManagerProvider() {
+            return MainActivity.this::getOverlayManager;
+        }
+
+        @Override
+        public TvInputManagerHelper getTvInputManagerHelperSingleton() {
+            return getTvInputManagerHelper();
+        }
+
+        @Override
+        public Provider<Long> getCurrentPlayingPositionProvider() {
+            return MainActivity.this::getCurrentPlayingPosition;
+        }
+
+        @Override
+        public DvrManager getDvrManagerSingleton() {
+            return TvSingletons.getSingletons(getApplicationContext()).getDvrManager();
+        }
+    }
+
+    /** Exports {@link MainActivity} for Dagger codegen to create the appropriate injector. */
+    @dagger.Module
+    public abstract static class Module {
+        @ContributesAndroidInjector
+        abstract MainActivity contributesMainActivityActivityInjector();
     }
 }
diff --git a/src/com/android/tv/MediaSessionWrapper.java b/src/com/android/tv/MediaSessionWrapper.java
index 43cd74d..a647a06 100644
--- a/src/com/android/tv/MediaSessionWrapper.java
+++ b/src/com/android/tv/MediaSessionWrapper.java
@@ -16,12 +16,14 @@
 
 package com.android.tv;
 
+import android.app.Activity;
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.media.MediaMetadata;
+import android.media.session.MediaController;
 import android.media.session.MediaSession;
 import android.media.session.PlaybackState;
 import android.media.tv.TvContract;
@@ -31,6 +33,7 @@
 import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
 import android.text.TextUtils;
+import android.util.Log;
 import com.android.tv.data.Program;
 import com.android.tv.data.api.Channel;
 import com.android.tv.util.Utils;
@@ -41,9 +44,12 @@
  * {@link MainActivity}.
  */
 class MediaSessionWrapper {
+    private static final String TAG = "MediaSessionWrapper";
+    private static final boolean DEBUG = false;
     private static final String MEDIA_SESSION_TAG = "com.android.tv.mediasession";
 
-    private static final PlaybackState MEDIA_SESSION_STATE_PLAYING =
+    @VisibleForTesting
+    static final PlaybackState MEDIA_SESSION_STATE_PLAYING =
             new PlaybackState.Builder()
                     .setState(
                             PlaybackState.STATE_PLAYING,
@@ -51,7 +57,8 @@
                             1.0f)
                     .build();
 
-    private static final PlaybackState MEDIA_SESSION_STATE_STOPPED =
+    @VisibleForTesting
+    static final PlaybackState MEDIA_SESSION_STATE_STOPPED =
             new PlaybackState.Builder()
                     .setState(
                             PlaybackState.STATE_STOPPED,
@@ -61,6 +68,20 @@
 
     private final Context mContext;
     private final MediaSession mMediaSession;
+    private final MediaController.Callback mMediaControllerCallback =
+            new MediaController.Callback() {
+                @Override
+                public void onPlaybackStateChanged(@Nullable PlaybackState state) {
+                    super.onPlaybackStateChanged(state);
+                    if (DEBUG) {
+                        Log.d(TAG, "onPlaybackStateChanged: " + state);
+                    }
+                    if (isMediaSessionStateStop(state)) {
+                        mMediaSession.setActive(false);
+                    }
+                }
+            };
+    private MediaController mMediaController;
     private int mNowPlayingCardWidth;
     private int mNowPlayingCardHeight;
 
@@ -79,6 +100,8 @@
                 MediaSession.FLAG_HANDLES_MEDIA_BUTTONS
                         | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
         mMediaSession.setSessionActivity(pendingIntent);
+
+        initMediaController();
         mNowPlayingCardWidth =
                 mContext.getResources().getDimensionPixelSize(R.dimen.notif_card_img_max_width);
         mNowPlayingCardHeight =
@@ -97,7 +120,6 @@
             mMediaSession.setPlaybackState(MEDIA_SESSION_STATE_PLAYING);
         } else if (mMediaSession.isActive()) {
             mMediaSession.setPlaybackState(MEDIA_SESSION_STATE_STOPPED);
-            mMediaSession.setActive(false);
         }
     }
 
@@ -150,6 +172,7 @@
      * @see MediaSession#release()
      */
     void release() {
+        unregisterMediaControllerCallback();
         mMediaSession.release();
     }
 
@@ -223,6 +246,30 @@
         return mMediaSession;
     }
 
+    @VisibleForTesting
+    MediaController.Callback getMediaControllerCallback() {
+        return mMediaControllerCallback;
+    }
+
+    @VisibleForTesting
+    void initMediaController() {
+        mMediaController = new MediaController(mContext, mMediaSession.getSessionToken());
+        ((Activity) mContext).setMediaController(mMediaController);
+        mMediaController.registerCallback(mMediaControllerCallback);
+    }
+
+    @VisibleForTesting
+    void unregisterMediaControllerCallback() {
+        mMediaController.unregisterCallback(mMediaControllerCallback);
+    }
+
+    private static boolean isMediaSessionStateStop(PlaybackState state) {
+        return state != null
+                && state.getState() == MEDIA_SESSION_STATE_STOPPED.getState()
+                && state.getPosition() == MEDIA_SESSION_STATE_STOPPED.getPosition()
+                && state.getPlaybackSpeed() == MEDIA_SESSION_STATE_STOPPED.getPlaybackSpeed();
+    }
+
     private static class ProgramPosterArtCallback
             extends ImageLoader.ImageLoaderCallback<MediaSessionWrapper> {
         private final Channel mChannel;
diff --git a/src/com/android/tv/SetupPassthroughActivity.java b/src/com/android/tv/SetupPassthroughActivity.java
index 199ea51..5185b12 100644
--- a/src/com/android/tv/SetupPassthroughActivity.java
+++ b/src/com/android/tv/SetupPassthroughActivity.java
@@ -28,11 +28,11 @@
 import android.util.Log;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.actions.InputSetupActionUtils;
-import com.android.tv.common.experiments.Experiments;
 import com.android.tv.data.ChannelDataManager;
 import com.android.tv.data.ChannelDataManager.Listener;
 import com.android.tv.data.epg.EpgFetcher;
 import com.android.tv.data.epg.EpgInputWhiteList;
+import com.android.tv.features.TvFeatures;
 import com.android.tv.util.SetupUtils;
 import com.android.tv.util.TvInputManagerHelper;
 import com.android.tv.util.Utils;
@@ -66,12 +66,10 @@
         Intent intent = getIntent();
         String inputId = intent.getStringExtra(InputSetupActionUtils.EXTRA_INPUT_ID);
         mTvInputInfo = inputManager.getTvInputInfo(inputId);
-        mEpgInputWhiteList = new EpgInputWhiteList(tvSingletons.getRemoteConfig());
+        mEpgInputWhiteList = new EpgInputWhiteList(tvSingletons.getCloudEpgFlags());
         mActivityAfterCompletion = InputSetupActionUtils.getExtraActivityAfter(intent);
         boolean needToFetchEpg =
-                mTvInputInfo != null
-                        && Utils.isInternalTvInput(this, mTvInputInfo.getId())
-                        && Experiments.CLOUD_EPG.get();
+                mTvInputInfo != null && Utils.isInternalTvInput(this, mTvInputInfo.getId());
         if (needToFetchEpg) {
             // In case when the activity is restored, this flag should be restored as well.
             mEpgFetcherDuringScan = true;
@@ -144,23 +142,30 @@
             finish();
             return;
         }
+        if (mTvInputInfo == null) {
+            Log.w(
+                    TAG,
+                    "There is no input with ID "
+                            + getIntent().getStringExtra(InputSetupActionUtils.EXTRA_INPUT_ID)
+                            + ".");
+            setResult(resultCode, data);
+            finish();
+            return;
+        }
         TvSingletons.getSingletons(this)
                 .getSetupUtils()
                 .onTvInputSetupFinished(
                         mTvInputInfo.getId(),
-                        new Runnable() {
-                            @Override
-                            public void run() {
-                                if (mActivityAfterCompletion != null) {
-                                    try {
-                                        startActivity(mActivityAfterCompletion);
-                                    } catch (ActivityNotFoundException e) {
-                                        Log.w(TAG, "Activity launch failed", e);
-                                    }
+                        () -> {
+                            if (mActivityAfterCompletion != null) {
+                                try {
+                                    startActivity(mActivityAfterCompletion);
+                                } catch (ActivityNotFoundException e) {
+                                    Log.w(TAG, "Activity launch failed", e);
                                 }
-                                setResult(resultCode, data);
-                                finish();
                             }
+                            setResult(resultCode, data);
+                            finish();
                         });
     }
 
@@ -178,15 +183,12 @@
         private final ChannelDataManager mChannelDataManager;
         private final Handler mHandler = new Handler(Looper.getMainLooper());
         private final Runnable mScanTimeoutRunnable =
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        Log.w(
-                                TAG,
-                                "No channels has been added for a while."
-                                        + " The scan might have finished unexpectedly.");
-                        onScanTimedOut();
-                    }
+                () -> {
+                    Log.w(
+                            TAG,
+                            "No channels has been added for a while."
+                                    + " The scan might have finished unexpectedly.");
+                    onScanTimedOut();
                 };
         private final Listener mChannelDataManagerListener =
                 new Listener() {
diff --git a/src/com/android/tv/TimeShiftManager.java b/src/com/android/tv/TimeShiftManager.java
index bb3574d..779e8df 100644
--- a/src/com/android/tv/TimeShiftManager.java
+++ b/src/com/android/tv/TimeShiftManager.java
@@ -17,7 +17,6 @@
 package com.android.tv;
 
 import android.annotation.SuppressLint;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.os.Handler;
 import android.os.Message;
@@ -35,7 +34,7 @@
 import com.android.tv.data.ProgramDataManager;
 import com.android.tv.data.api.Channel;
 import com.android.tv.ui.TunableTvView;
-import com.android.tv.ui.TunableTvViewPlayingApi.TimeShiftListener;
+import com.android.tv.ui.api.TunableTvViewPlayingApi.TimeShiftListener;
 import com.android.tv.util.AsyncDbTask;
 import com.android.tv.util.TimeShiftUtils;
 import com.android.tv.util.Utils;
@@ -87,16 +86,15 @@
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(
-        flag = true,
-        value = {
-            TIME_SHIFT_ACTION_ID_PLAY,
-            TIME_SHIFT_ACTION_ID_PAUSE,
-            TIME_SHIFT_ACTION_ID_REWIND,
-            TIME_SHIFT_ACTION_ID_FAST_FORWARD,
-            TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS,
-            TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT
-        }
-    )
+            flag = true,
+            value = {
+                TIME_SHIFT_ACTION_ID_PLAY,
+                TIME_SHIFT_ACTION_ID_PAUSE,
+                TIME_SHIFT_ACTION_ID_REWIND,
+                TIME_SHIFT_ACTION_ID_FAST_FORWARD,
+                TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS,
+                TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT
+            })
     public @interface TimeShiftActionId {}
 
     public static final int TIME_SHIFT_ACTION_ID_PLAY = 1;
@@ -715,7 +713,7 @@
                                 : mRecordEndTimeMs;
                 long currentPositionMs =
                         Math.max(
-                                Math.min(mTvView.timeshiftGetCurrentPositionMs(), currentTimeMs),
+                                Math.min(mTvView.timeShiftGetCurrentPositionMs(), currentTimeMs),
                                 mRecordStartTimeMs);
                 boolean isCurrentTime =
                         currentTimeMs - currentPositionMs < RECORDING_BOUNDARY_THRESHOLD;
@@ -723,7 +721,7 @@
                 if (isCurrentTime && isForwarding()) {
                     // It's playing forward and the current playing position reached
                     // the current system time. i.e. The live stream is played.
-                    // Therefore no need to call TvView.timeshiftGetCurrentPositionMs
+                    // Therefore no need to call TvView.timeShiftGetCurrentPositionMs
                     // any more.
                     newCurrentPositionMs = currentTimeMs;
                     mIsPlayOffsetChanged = false;
@@ -753,14 +751,14 @@
             mDisplayedPlaySpeed = PLAY_SPEED_1X;
             mPlaybackSpeed = 1;
             mPlayDirection = PLAY_DIRECTION_FORWARD;
-            mTvView.timeshiftPlay();
+            mTvView.timeShiftPlay();
             setPlayStatus(PLAY_STATUS_PLAYING);
         }
 
         void pause() {
             mDisplayedPlaySpeed = PLAY_SPEED_1X;
             mPlaybackSpeed = 1;
-            mTvView.timeshiftPause();
+            mTvView.timeShiftPause();
             setPlayStatus(PLAY_STATUS_PAUSED);
             mIsPlayOffsetChanged = true;
         }
@@ -783,7 +781,7 @@
             }
             mPlayDirection = PLAY_DIRECTION_BACKWARD;
             mPlaybackSpeed = getPlaybackSpeed();
-            mTvView.timeshiftRewind(mPlaybackSpeed);
+            mTvView.timeShiftRewind(mPlaybackSpeed);
             setPlayStatus(PLAY_STATUS_PLAYING);
             mIsPlayOffsetChanged = true;
         }
@@ -796,14 +794,14 @@
             }
             mPlayDirection = PLAY_DIRECTION_FORWARD;
             mPlaybackSpeed = getPlaybackSpeed();
-            mTvView.timeshiftFastForward(mPlaybackSpeed);
+            mTvView.timeShiftFastForward(mPlaybackSpeed);
             setPlayStatus(PLAY_STATUS_PLAYING);
             mIsPlayOffsetChanged = true;
         }
 
         /** Moves to the specified time. */
         void seekTo(long timeMs) {
-            mTvView.timeshiftSeekTo(
+            mTvView.timeShiftSeekTo(
                     Math.min(
                             mRecordEndTimeMs == CURRENT_TIME
                                     ? System.currentTimeMillis()
@@ -821,9 +819,9 @@
             if (playbackSpeed != mPlaybackSpeed) {
                 mPlaybackSpeed = playbackSpeed;
                 if (mPlayDirection == PLAY_DIRECTION_FORWARD) {
-                    mTvView.timeshiftFastForward(mPlaybackSpeed);
+                    mTvView.timeShiftFastForward(mPlaybackSpeed);
                 } else {
-                    mTvView.timeshiftRewind(mPlaybackSpeed);
+                    mTvView.timeShiftRewind(mPlaybackSpeed);
                 }
             }
         }
@@ -977,8 +975,7 @@
                 }
             }
             if (mChannel != null) {
-                mProgramLoadTask =
-                        new LoadProgramsForCurrentChannelTask(mContext.getContentResolver(), next);
+                mProgramLoadTask = new LoadProgramsForCurrentChannelTask(next);
                 mProgramLoadTask.executeOnDbThread();
             }
         }
@@ -1225,10 +1222,10 @@
         private class LoadProgramsForCurrentChannelTask
                 extends AsyncDbTask.LoadProgramsForChannelTask {
 
-            LoadProgramsForCurrentChannelTask(ContentResolver contentResolver, Range<Long> period) {
+            LoadProgramsForCurrentChannelTask(Range<Long> period) {
                 super(
                         TvSingletons.getSingletons(mContext).getDbExecutor(),
-                        contentResolver,
+                        mContext,
                         mChannel.getId(),
                         period);
             }
@@ -1309,13 +1306,7 @@
                     mProgramLoadTask = null;
                 }
                 // Need to post to handler, because the task is still running.
-                mHandler.post(
-                        new Runnable() {
-                            @Override
-                            public void run() {
-                                startTaskIfNeeded();
-                            }
-                        });
+                mHandler.post(ProgramManager.this::startTaskIfNeeded);
             }
 
             boolean overlaps(Queue<Range<Long>> programLoadQueue) {
diff --git a/src/com/android/tv/TvApplication.java b/src/com/android/tv/TvApplication.java
index 826317b..5f25a24 100644
--- a/src/com/android/tv/TvApplication.java
+++ b/src/com/android/tv/TvApplication.java
@@ -34,8 +34,8 @@
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.KeyEvent;
+import android.widget.Toast;
 import com.android.tv.common.BaseApplication;
-import com.android.tv.common.concurrent.NamedThreadFactory;
 import com.android.tv.common.feature.CommonFeatures;
 import com.android.tv.common.recording.RecordingStorageStatusManager;
 import com.android.tv.common.ui.setup.animation.SetupAnimationHelper;
@@ -55,17 +55,22 @@
 import com.android.tv.dvr.DvrWatchedPositionManager;
 import com.android.tv.dvr.recorder.RecordingScheduler;
 import com.android.tv.dvr.ui.browse.DvrBrowseActivity;
+import com.android.tv.features.TvFeatures;
+import com.android.tv.perf.PerformanceMonitorManager;
+import com.android.tv.perf.PerformanceMonitorManagerFactory;
 import com.android.tv.recommendation.ChannelPreviewUpdater;
 import com.android.tv.recommendation.RecordedProgramPreviewUpdater;
-import com.android.tv.tuner.TunerInputController;
-import com.android.tv.tuner.util.TunerInputInfoUtils;
+import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
+import com.android.tv.tunerinputcontroller.TunerInputController;
+import com.android.tv.util.AsyncDbTask.DbExecutor;
 import com.android.tv.util.SetupUtils;
 import com.android.tv.util.TvInputManagerHelper;
 import com.android.tv.util.Utils;
+import com.google.common.base.Optional;
+import dagger.Lazy;
 import java.util.List;
 import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import javax.inject.Inject;
 
 /**
  * Live TV application.
@@ -73,6 +78,9 @@
  * <p>This includes all the Google specific hooks.
  */
 public abstract class TvApplication extends BaseApplication implements TvSingletons, Starter {
+
+    protected static final PerformanceMonitorManager PERFORMANCE_MONITOR_MANAGER =
+            PerformanceMonitorManagerFactory.create();
     private static final String TAG = "TvApplication";
     private static final boolean DEBUG = false;
 
@@ -89,10 +97,6 @@
 
     private static final String PREFERENCE_IS_FIRST_LAUNCH = "is_first_launch";
 
-    private static final NamedThreadFactory THREAD_FACTORY = new NamedThreadFactory("tv-app-db");
-    private static final ExecutorService DB_EXECUTOR =
-            Executors.newSingleThreadExecutor(THREAD_FACTORY);
-
     private String mVersionName = "";
 
     private final MainActivityWrapper mMainActivityWrapper = new MainActivityWrapper();
@@ -111,22 +115,28 @@
     // STOP-SHIP: Remove this variable when Tuner Process is split to another application.
     // When this variable is null, we don't know in which process TvApplication runs.
     private Boolean mRunningInMainProcess;
-    private TvInputManagerHelper mTvInputManagerHelper;
+    @Inject Lazy<TvInputManagerHelper> mLazyTvInputManagerHelper;
     private boolean mStarted;
     private EpgFetcher mEpgFetcher;
-    private TunerInputController mTunerInputController;
+
+    @Inject Optional<BuiltInTunerManager> mOptionalBuiltInTunerManager;
+    @Inject SetupUtils mSetupUtils;
+    @Inject @DbExecutor Executor mDbExecutor;
 
     @Override
     public void onCreate() {
+        if (getSystemService(TvInputManager.class) == null) {
+            String msg = "Not an Android TV device.";
+            Toast.makeText(this, msg, Toast.LENGTH_LONG);
+            Log.wtf(TAG, msg);
+            throw new IllegalStateException(msg);
+        }
         super.onCreate();
         SharedPreferencesUtils.initialize(
                 this,
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        if (mRunningInMainProcess != null && mRunningInMainProcess) {
-                            checkTunerServiceOnFirstLaunch();
-                        }
+                () -> {
+                    if (mRunningInMainProcess != null && mRunningInMainProcess) {
+                        checkTunerServiceOnFirstLaunch();
                     }
                 });
         try {
@@ -164,13 +174,19 @@
                             new TvInputCallback() {
                                 @Override
                                 public void onInputAdded(String inputId) {
-                                    if (TvFeatures.TUNER.isEnabled(TvApplication.this)
-                                            && TextUtils.equals(
-                                                    inputId, getEmbeddedTunerInputId())) {
-                                        TunerInputInfoUtils.updateTunerInputInfo(
-                                                TvApplication.this);
+                                    if (mOptionalBuiltInTunerManager.isPresent()) {
+                                        BuiltInTunerManager builtInTunerManager =
+                                                mOptionalBuiltInTunerManager.get();
+                                        if (TextUtils.equals(
+                                                inputId,
+                                                builtInTunerManager.getEmbeddedTunerInputId())) {
+
+                                            builtInTunerManager
+                                                    .getTunerInputController()
+                                                    .updateTunerInputInfo(TvApplication.this);
+                                        }
+                                        handleInputCountChanged();
                                     }
-                                    handleInputCountChanged();
                                 }
 
                                 @Override
@@ -178,10 +194,13 @@
                                     handleInputCountChanged();
                                 }
                             });
-            if (TvFeatures.TUNER.isEnabled(this)) {
+            if (mOptionalBuiltInTunerManager.isPresent()) {
                 // If the tuner input service is added before the app is started, we need to
                 // handle it here.
-                TunerInputInfoUtils.updateTunerInputInfo(TvApplication.this);
+                mOptionalBuiltInTunerManager
+                        .get()
+                        .getTunerInputController()
+                        .updateTunerInputInfo(TvApplication.this);
             }
             if (CommonFeatures.DVR.isEnabled(this)) {
                 mDvrScheduleManager = new DvrScheduleManager(this);
@@ -205,8 +224,12 @@
         boolean isFirstLaunch = sharedPreferences.getBoolean(PREFERENCE_IS_FIRST_LAUNCH, true);
         if (isFirstLaunch) {
             if (DEBUG) Log.d(TAG, "Congratulations, it's the first launch!");
-            getTunerInputController()
-                    .onCheckingUsbTunerStatus(this, ACTION_APPLICATION_FIRST_LAUNCHED);
+            if (mOptionalBuiltInTunerManager.isPresent()) {
+                mOptionalBuiltInTunerManager
+                        .get()
+                        .getTunerInputController()
+                        .onCheckingUsbTunerStatus(this, ACTION_APPLICATION_FIRST_LAUNCHED);
+            }
             SharedPreferences.Editor editor = sharedPreferences.edit();
             editor.putBoolean(PREFERENCE_IS_FIRST_LAUNCH, false);
             editor.apply();
@@ -220,7 +243,7 @@
 
     @Override
     public synchronized SetupUtils getSetupUtils() {
-        return SetupUtils.createForTvSingletons(this);
+        return mSetupUtils;
     }
 
     /** Returns the {@link DvrManager}. */
@@ -282,13 +305,10 @@
             return mProgramDataManager;
         }
         Utils.runInMainThreadAndWait(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        if (mProgramDataManager == null) {
-                            mProgramDataManager = new ProgramDataManager(TvApplication.this);
-                            mProgramDataManager.start();
-                        }
+                () -> {
+                    if (mProgramDataManager == null) {
+                        mProgramDataManager = new ProgramDataManager(TvApplication.this);
+                        mProgramDataManager.start();
                     }
                 });
         return mProgramDataManager;
@@ -340,21 +360,7 @@
     /** Returns {@link TvInputManagerHelper}. */
     @Override
     public TvInputManagerHelper getTvInputManagerHelper() {
-        if (mTvInputManagerHelper == null) {
-            mTvInputManagerHelper = new TvInputManagerHelper(this);
-            mTvInputManagerHelper.start();
-        }
-        return mTvInputManagerHelper;
-    }
-
-    @Override
-    public synchronized TunerInputController getTunerInputController() {
-        if (mTunerInputController == null) {
-            mTunerInputController =
-                    new TunerInputController(
-                            ComponentName.unflattenFromString(getEmbeddedTunerInputId()));
-        }
-        return mTunerInputController;
+        return mLazyTvInputManagerHelper.get();
     }
 
     @Override
@@ -480,12 +486,16 @@
         if (!enable) {
             List<TvInputInfo> inputs = inputManager.getTvInputList();
             boolean skipTunerInputCheck = false;
+            Optional<String> optionalEmbeddedTunerInputId =
+                    mOptionalBuiltInTunerManager.transform(
+                            BuiltInTunerManager::getEmbeddedTunerInputId);
             // Enable the TvActivity only if there is at least one tuner type input.
             if (!skipTunerInputCheck) {
                 for (TvInputInfo input : inputs) {
                     if (calledByTunerServiceChanged
                             && !tunerServiceEnabled
-                            && getEmbeddedTunerInputId().equals(input.getId())) {
+                            && optionalEmbeddedTunerInputId.isPresent()
+                            && optionalEmbeddedTunerInputId.get().equals(input.getId())) {
                         continue;
                     }
                     if (input.getType() == TvInputInfo.TYPE_TUNER) {
@@ -507,11 +517,11 @@
                     name, newState, dontKillApp ? PackageManager.DONT_KILL_APP : 0);
             Log.i(TAG, (enable ? "Un-hide" : "Hide") + " Live TV.");
         }
-        getSetupUtils().onInputListUpdated(inputManager);
+        mSetupUtils.onInputListUpdated(inputManager);
     }
 
     @Override
     public Executor getDbExecutor() {
-        return DB_EXECUTOR;
+        return mDbExecutor;
     }
 }
diff --git a/src/com/android/tv/TvSingletons.java b/src/com/android/tv/TvSingletons.java
index 0c7f78a..20edf3d 100644
--- a/src/com/android/tv/TvSingletons.java
+++ b/src/com/android/tv/TvSingletons.java
@@ -22,6 +22,7 @@
 import com.android.tv.common.BaseApplication;
 import com.android.tv.common.BaseSingletons;
 import com.android.tv.common.experiments.ExperimentLoader;
+import com.android.tv.common.flags.has.HasUiFlags;
 import com.android.tv.data.ChannelDataManager;
 import com.android.tv.data.PreviewDataManager;
 import com.android.tv.data.ProgramDataManager;
@@ -33,17 +34,23 @@
 import com.android.tv.dvr.DvrWatchedPositionManager;
 import com.android.tv.dvr.recorder.RecordingScheduler;
 import com.android.tv.perf.PerformanceMonitor;
-import com.android.tv.tuner.TunerInputController;
+import com.android.tv.tunerinputcontroller.HasBuiltInTunerManager;
 import com.android.tv.util.SetupUtils;
 import com.android.tv.util.TvInputManagerHelper;
 import com.android.tv.util.account.AccountHelper;
+import com.android.tv.common.flags.BackendKnobsFlags;
 import java.util.concurrent.Executor;
 import javax.inject.Provider;
 
 /** Interface with getters for application scoped singletons. */
-public interface TvSingletons extends BaseSingletons {
+public interface TvSingletons extends BaseSingletons, HasBuiltInTunerManager, HasUiFlags {
 
-    /** Returns the @{@link TvSingletons} using the application context. */
+    /**
+     * Returns the @{@link TvSingletons} using the application context.
+     *
+     * @deprecated use injection instead.
+     */
+    @Deprecated
     static TvSingletons getSingletons(Context context) {
         return (TvSingletons) BaseApplication.getSingletons(context);
     }
@@ -52,6 +59,7 @@
 
     void handleInputCountChanged();
 
+    @Deprecated
     ChannelDataManager getChannelDataManager();
 
     /**
@@ -60,6 +68,8 @@
      */
     boolean isChannelDataManagerLoadFinished();
 
+    /** @deprecated use injection instead. */
+    @Deprecated
     ProgramDataManager getProgramDataManager();
 
     /**
@@ -92,17 +102,23 @@
 
     PerformanceMonitor getPerformanceMonitor();
 
+    /** @deprecated use injection instead. */
+    @Deprecated
     TvInputManagerHelper getTvInputManagerHelper();
 
     Provider<EpgReader> providesEpgReader();
 
     EpgFetcher getEpgFetcher();
 
+    /** @deprecated use injection instead. */
+    @Deprecated
     SetupUtils getSetupUtils();
 
-    TunerInputController getTunerInputController();
-
     ExperimentLoader getExperimentLoader();
 
+    /** @deprecated use injection instead. */
+    @Deprecated
     Executor getDbExecutor();
+
+    BackendKnobsFlags getBackendKnobs();
 }
diff --git a/src/com/android/tv/analytics/SendChannelStatusRunnable.java b/src/com/android/tv/analytics/SendChannelStatusRunnable.java
index 4a84434..306bd85 100644
--- a/src/com/android/tv/analytics/SendChannelStatusRunnable.java
+++ b/src/com/android/tv/analytics/SendChannelStatusRunnable.java
@@ -43,13 +43,7 @@
         final SendChannelStatusRunnable sendChannelStatusRunnable =
                 new SendChannelStatusRunnable(channelDataManager, tracker);
 
-        Runnable onStopRunnable =
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        sendChannelStatusRunnable.setDbLoadListener(null);
-                    }
-                };
+        Runnable onStopRunnable = () -> sendChannelStatusRunnable.setDbLoadListener(null);
         final RecurringRunner recurringRunner =
                 new RecurringRunner(
                         context,
@@ -70,14 +64,7 @@
                             // done
                             // via a post on the main thread
                             new Handler(Looper.getMainLooper())
-                                    .post(
-                                            new Runnable() {
-                                                @Override
-                                                public void run() {
-                                                    sendChannelStatusRunnable.setDbLoadListener(
-                                                            null);
-                                                }
-                                            });
+                                    .post(() -> sendChannelStatusRunnable.setDbLoadListener(null));
                             recurringRunner.start();
                         }
 
diff --git a/src/com/android/tv/app/LiveTvApplication.java b/src/com/android/tv/app/LiveTvApplication.java
index 461331d..38e85e4 100644
--- a/src/com/android/tv/app/LiveTvApplication.java
+++ b/src/com/android/tv/app/LiveTvApplication.java
@@ -16,36 +16,37 @@
 
 package com.android.tv.app;
 
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.media.tv.TvContract;
 import com.android.tv.TvApplication;
+import com.android.tv.TvSingletons;
 import com.android.tv.analytics.Analytics;
 import com.android.tv.analytics.StubAnalytics;
 import com.android.tv.analytics.Tracker;
-import com.android.tv.common.CommonConstants;
-import com.android.tv.common.actions.InputSetupActionUtils;
-import com.android.tv.common.config.DefaultConfigManager;
-import com.android.tv.common.config.api.RemoteConfig;
+import com.android.tv.common.dagger.ApplicationModule;
 import com.android.tv.common.experiments.ExperimentLoader;
-import com.android.tv.common.util.CommonUtils;
+import com.android.tv.common.flags.impl.DefaultBackendKnobsFlags;
+import com.android.tv.common.flags.impl.DefaultCloudEpgFlags;
+import com.android.tv.common.flags.impl.DefaultConcurrentDvrPlaybackFlags;
+import com.android.tv.common.flags.impl.DefaultUiFlags;
+import com.android.tv.common.singletons.HasSingletons;
 import com.android.tv.data.epg.EpgReader;
 import com.android.tv.data.epg.StubEpgReader;
+import com.android.tv.modules.TvSingletonsModule;
 import com.android.tv.perf.PerformanceMonitor;
-import com.android.tv.perf.StubPerformanceMonitor;
-import com.android.tv.tuner.livetuner.LiveTvTunerTvInputService;
-import com.android.tv.tuner.setup.LiveTvTunerSetupActivity;
+import com.android.tv.perf.PerformanceMonitorManagerFactory;
+import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
 import com.android.tv.util.account.AccountHelper;
 import com.android.tv.util.account.AccountHelperImpl;
+import com.google.common.base.Optional;
+import dagger.android.AndroidInjector;
 import javax.inject.Provider;
 
 /** The top level application for Live TV. */
-public class LiveTvApplication extends TvApplication {
-    protected static final String TV_ACTIVITY_CLASS_NAME =
-            CommonConstants.BASE_PACKAGE + ".TvActivity";
+public class LiveTvApplication extends TvApplication implements HasSingletons<TvSingletons> {
 
-    private final StubPerformanceMonitor performanceMonitor = new StubPerformanceMonitor();
+    static {
+        PERFORMANCE_MONITOR_MANAGER.getStartupMeasure().onAppClassLoaded();
+    }
+
     private final Provider<EpgReader> mEpgReaderProvider =
             new Provider<EpgReader>() {
 
@@ -55,12 +56,30 @@
                 }
             };
 
+    private final DefaultBackendKnobsFlags mBackendKnobsFlags = new DefaultBackendKnobsFlags();
+    private final DefaultCloudEpgFlags mCloudEpgFlags = new DefaultCloudEpgFlags();
+    private final DefaultUiFlags mUiFlags = new DefaultUiFlags();
+    private final DefaultConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags =
+            new DefaultConcurrentDvrPlaybackFlags();
     private AccountHelper mAccountHelper;
     private Analytics mAnalytics;
     private Tracker mTracker;
-    private String mEmbeddedInputId;
-    private RemoteConfig mRemoteConfig;
     private ExperimentLoader mExperimentLoader;
+    private PerformanceMonitor mPerformanceMonitor;
+
+    @Override
+    protected AndroidInjector<LiveTvApplication> applicationInjector() {
+        return DaggerLiveTvApplicationComponent.builder()
+                .applicationModule(new ApplicationModule(this))
+                .tvSingletonsModule(new TvSingletonsModule(this))
+                .build();
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        PERFORMANCE_MONITOR_MANAGER.getStartupMeasure().onAppCreate(this);
+    }
 
     /** Returns the {@link AccountHelperImpl}. */
     @Override
@@ -73,7 +92,10 @@
 
     @Override
     public synchronized PerformanceMonitor getPerformanceMonitor() {
-        return performanceMonitor;
+        if (mPerformanceMonitor == null) {
+            mPerformanceMonitor = PerformanceMonitorManagerFactory.create().initialize(this);
+        }
+        return mPerformanceMonitor;
     }
 
     @Override
@@ -87,6 +109,11 @@
         return mExperimentLoader;
     }
 
+    @Override
+    public DefaultBackendKnobsFlags getBackendKnobs() {
+        return mBackendKnobsFlags;
+    }
+
     /** Returns the {@link Analytics}. */
     @Override
     public synchronized Analytics getAnalytics() {
@@ -106,34 +133,32 @@
     }
 
     @Override
-    public Intent getTunerSetupIntent(Context context) {
-        // Make an intent to launch the setup activity of TV tuner input.
-        Intent intent =
-                CommonUtils.createSetupIntent(
-                        new Intent(context, LiveTvTunerSetupActivity.class), mEmbeddedInputId);
-        intent.putExtra(InputSetupActionUtils.EXTRA_INPUT_ID, mEmbeddedInputId);
-        Intent tvActivityIntent = new Intent();
-        tvActivityIntent.setComponent(new ComponentName(context, TV_ACTIVITY_CLASS_NAME));
-        intent.putExtra(InputSetupActionUtils.EXTRA_ACTIVITY_AFTER_COMPLETION, tvActivityIntent);
-        return intent;
+    public DefaultCloudEpgFlags getCloudEpgFlags() {
+        return mCloudEpgFlags;
     }
 
     @Override
-    public synchronized String getEmbeddedTunerInputId() {
-        if (mEmbeddedInputId == null) {
-            mEmbeddedInputId =
-                    TvContract.buildInputId(
-                            new ComponentName(this, LiveTvTunerTvInputService.class));
-        }
-        return mEmbeddedInputId;
+    public DefaultUiFlags getUiFlags() {
+        return mUiFlags;
     }
 
     @Override
-    public RemoteConfig getRemoteConfig() {
-        if (mRemoteConfig == null) {
-            // No need to synchronize this, it does not hurt to create two and throw one away.
-            mRemoteConfig = DefaultConfigManager.createInstance(this).getRemoteConfig();
-        }
-        return mRemoteConfig;
+    public Optional<BuiltInTunerManager> getBuiltInTunerManager() {
+        return Optional.absent();
+    }
+
+    @Override
+    public BuildType getBuildType() {
+        return BuildType.AOSP;
+    }
+
+    @Override
+    public DefaultConcurrentDvrPlaybackFlags getConcurrentDvrPlaybackFlags() {
+        return mConcurrentDvrPlaybackFlags;
+    }
+
+    @Override
+    public TvSingletons singletons() {
+        return this;
     }
 }
diff --git a/src/com/android/tv/app/LiveTvApplicationComponent.java b/src/com/android/tv/app/LiveTvApplicationComponent.java
new file mode 100644
index 0000000..3d3f049
--- /dev/null
+++ b/src/com/android/tv/app/LiveTvApplicationComponent.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.app;
+
+import dagger.Component;
+import dagger.android.AndroidInjectionModule;
+import dagger.android.AndroidInjector;
+import javax.inject.Singleton;
+
+/** Dagger component for {@link LiveTvApplication}. */
+@Singleton
+@Component(modules = {AndroidInjectionModule.class, LiveTvModule.class})
+public interface LiveTvApplicationComponent extends AndroidInjector<LiveTvApplication> {}
diff --git a/src/com/android/tv/app/LiveTvModule.java b/src/com/android/tv/app/LiveTvModule.java
new file mode 100644
index 0000000..a28749b
--- /dev/null
+++ b/src/com/android/tv/app/LiveTvModule.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.app;
+
+import com.android.tv.common.flags.impl.DefaultFlagsModule;
+import com.android.tv.modules.TvApplicationModule;
+import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
+import com.google.common.base.Optional;
+import dagger.Module;
+import dagger.Provides;
+
+/** Dagger module for {@link LiveTvApplication}. */
+@Module(includes = {DefaultFlagsModule.class, TvApplicationModule.class})
+class LiveTvModule {
+
+    @Provides
+    Optional<BuiltInTunerManager> providesBuiltInTunerManager() {
+        return Optional.absent();
+    }
+}
diff --git a/src/com/android/tv/audio/AudioManagerHelper.java b/src/com/android/tv/audio/AudioManagerHelper.java
new file mode 100644
index 0000000..4acff2d
--- /dev/null
+++ b/src/com/android/tv/audio/AudioManagerHelper.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.audio;
+
+import android.app.Activity;
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioFocusRequest;
+import android.media.AudioManager;
+import android.os.Build;
+import android.support.annotation.Nullable;
+import com.android.tv.features.TvFeatures;
+import com.android.tv.ui.api.TunableTvViewPlayingApi;
+
+/** A helper class to help {@code Activities} to handle audio-related stuffs. */
+public class AudioManagerHelper implements AudioManager.OnAudioFocusChangeListener {
+    private static final float AUDIO_MAX_VOLUME = 1.0f;
+    private static final float AUDIO_MIN_VOLUME = 0.0f;
+    private static final float AUDIO_DUCKING_VOLUME = 0.3f;
+
+    private final Activity mActivity;
+    private final TunableTvViewPlayingApi mTvView;
+    private final AudioManager mAudioManager;
+    @Nullable private final AudioFocusRequest mFocusRequest;
+
+    private int mAudioFocusStatus = AudioManager.AUDIOFOCUS_NONE;
+
+    public AudioManagerHelper(Activity activity, TunableTvViewPlayingApi tvView) {
+        mActivity = activity;
+        mTvView = tvView;
+        mAudioManager = (AudioManager) activity.getSystemService(Context.AUDIO_SERVICE);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            mFocusRequest =
+                    new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
+                            .setAudioAttributes(
+                                    new AudioAttributes.Builder()
+                                            .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
+                                            .setUsage(AudioAttributes.USAGE_MEDIA)
+                                            .build())
+                            .setOnAudioFocusChangeListener(this)
+                            // Auto ducking from the system does not mute the TV Input Service.
+                            // Using will pause when ducked allows us to set the stream volume
+                            // even when we are not pausing.
+                            .setWillPauseWhenDucked(true)
+                            .build();
+        } else {
+            mFocusRequest = null;
+        }
+    }
+
+    /**
+     * Sets suitable volume to {@link TunableTvViewPlayingApi} according to the current audio focus.
+     *
+     * <p>If the focus status is {@link AudioManager#AUDIOFOCUS_LOSS} or {@link
+     * AudioManager#AUDIOFOCUS_NONE} and the activity is under PIP mode, this method will finish the
+     * activity. Sets suitable volume to {@link TunableTvViewPlayingApi} according to the current
+     * audio focus. If the focus status is {@link AudioManager#AUDIOFOCUS_LOSS} and the activity is
+     * under PIP mode, this method will finish the activity.
+     */
+    public void setVolumeByAudioFocusStatus() {
+        if (mTvView.isPlaying()) {
+            switch (mAudioFocusStatus) {
+                case AudioManager.AUDIOFOCUS_GAIN:
+                    if (mTvView.isTimeShiftAvailable()) {
+                        mTvView.timeShiftPlay();
+                    } else {
+                        mTvView.setStreamVolume(AUDIO_MAX_VOLUME);
+                    }
+                    break;
+                case AudioManager.AUDIOFOCUS_NONE:
+                case AudioManager.AUDIOFOCUS_LOSS:
+                    if (TvFeatures.PICTURE_IN_PICTURE.isEnabled(mActivity)
+                            && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
+                            && mActivity.isInPictureInPictureMode()) {
+                        mActivity.finish();
+                        break;
+                    }
+                    // fall through
+                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+                    if (mTvView.isTimeShiftAvailable()) {
+                        mTvView.timeShiftPause();
+                    } else {
+                        mTvView.setStreamVolume(AUDIO_MIN_VOLUME);
+                    }
+                    break;
+                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+                    if (mTvView.isTimeShiftAvailable()) {
+                        mTvView.timeShiftPause();
+                    } else {
+                        mTvView.setStreamVolume(AUDIO_DUCKING_VOLUME);
+                    }
+                    break;
+            }
+        }
+    }
+
+    /**
+     * Tries to request audio focus from {@link AudioManager} and set volume according to the
+     * returned result.
+     */
+    public void requestAudioFocus() {
+        int result;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            result = mAudioManager.requestAudioFocus(mFocusRequest);
+        } else {
+            result =
+                    mAudioManager.requestAudioFocus(
+                            this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
+        }
+        mAudioFocusStatus =
+                (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
+                        ? AudioManager.AUDIOFOCUS_GAIN
+                        : AudioManager.AUDIOFOCUS_LOSS;
+        setVolumeByAudioFocusStatus();
+    }
+
+    /** Abandons audio focus. */
+    public void abandonAudioFocus() {
+        mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            mAudioManager.abandonAudioFocusRequest(mFocusRequest);
+        } else {
+            mAudioManager.abandonAudioFocus(this);
+        }
+    }
+
+    @Override
+    public void onAudioFocusChange(int focusChange) {
+        mAudioFocusStatus = focusChange;
+        setVolumeByAudioFocusStatus();
+    }
+}
diff --git a/src/com/android/tv/audiotvservice/AudioOnlyTvService.java b/src/com/android/tv/audiotvservice/AudioOnlyTvService.java
new file mode 100644
index 0000000..5d0e9c8
--- /dev/null
+++ b/src/com/android/tv/audiotvservice/AudioOnlyTvService.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.audiotvservice;
+
+import android.app.Notification;
+import android.app.Service;
+import android.content.Intent;
+import android.media.session.MediaSession;
+import android.net.Uri;
+import android.os.IBinder;
+import android.support.annotation.Nullable;
+import android.util.Log;
+import com.android.tv.data.ChannelImpl;
+import com.android.tv.data.StreamInfo;
+import com.android.tv.data.api.Channel;
+import com.android.tv.ui.TunableTvView;
+import com.android.tv.ui.TunableTvView.OnTuneListener;
+
+/** Foreground service for audio-only TV inputs. */
+public class AudioOnlyTvService extends Service implements OnTuneListener {
+    // TODO(b/110969180): implement this service.
+    private static final String TAG = "AudioOnlyTvService";
+    private static final int NOTIFICATION_ID = 1;
+
+    @Nullable private String mTvInputId;
+    private TunableTvView mTvView;
+    // TODO(b/110969180): perhaps use MediaSessionWrapper
+    private MediaSession mMediaSession;
+
+    @Nullable
+    @Override
+    public IBinder onBind(Intent intent) {
+        Log.i(TAG, "onBind");
+        return null;
+    }
+
+    @Override
+    public void onCreate() {
+        Log.i(TAG, "onCreate");
+        // TODO(b/110969180): create TvView
+
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        Log.i(TAG, "onStartCommand. flags = " + flags + ", startId = " + startId);
+        // TODO(b/110969180): real notification and or media session
+        startForeground(NOTIFICATION_ID, new Notification());
+        mTvInputId = AudioOnlyTvServiceUtil.getInputIdFromIntent(intent);
+        tune(mTvInputId);
+        return START_STICKY;
+    }
+
+    private void tune(String tvInputId) {
+        Channel channel = ChannelImpl.createPassthroughChannel(tvInputId);
+        mTvView.tuneTo(channel, null, this);
+    }
+
+    @Override
+    public void onDestroy() {
+        Log.i(TAG, "onDestroy");
+        mTvInputId = null;
+        // TODO(b/110969180): clear TvView
+    }
+
+    // TODO(b/110969180): figure out when to stop ourselves,  mediaSession event?
+
+    // TODO(b/110969180): handle OnTuner Listener
+    @Override
+    public void onTuneFailed(Channel channel) {}
+
+    @Override
+    public void onUnexpectedStop(Channel channel) {}
+
+    @Override
+    public void onStreamInfoChanged(StreamInfo info, boolean allowAutoSelectionOfTrack) {}
+
+    @Override
+    public void onChannelRetuned(Uri channel) {}
+
+    @Override
+    public void onContentBlocked() {}
+
+    @Override
+    public void onContentAllowed() {}
+
+    @Override
+    public void onChannelSignalStrength() {}
+}
diff --git a/src/com/android/tv/audiotvservice/AudioOnlyTvServiceUtil.java b/src/com/android/tv/audiotvservice/AudioOnlyTvServiceUtil.java
new file mode 100644
index 0000000..7ffe883
--- /dev/null
+++ b/src/com/android/tv/audiotvservice/AudioOnlyTvServiceUtil.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.audiotvservice;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+/** Utility methods to start and stop audio only TV Player. */
+public final class AudioOnlyTvServiceUtil {
+    private static final String TAG = "AudioOnlyTvServiceUtil";
+    private static final String EXTRA_INPUT_ID = "intputId";
+
+    @MainThread
+    public static void startAudioOnlyInput(Context context, String tvInputId) {
+        Log.i(TAG, "startAudioOnlyInput");
+        Intent intent = getIntent(context);
+        if (intent == null) {
+            return;
+        }
+        intent.putExtra(EXTRA_INPUT_ID, tvInputId);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            context.startForegroundService(intent);
+        } else {
+            context.startService(intent);
+        }
+    }
+
+    @Nullable
+    private static Intent getIntent(Context context) {
+        try {
+            return new Intent(
+                    context, Class.forName("com.android.tv.audiotvservice.AudioOnlyTvService"));
+        } catch (ClassNotFoundException e) {
+            Log.wtf(TAG, e);
+            return null;
+        }
+    }
+
+    @MainThread
+    public static void stopAudioOnlyInput(Context context) {
+        Log.i(TAG, "stopForegroundService");
+        context.stopService(getIntent(context));
+    }
+
+    @Nullable
+    public static String getInputIdFromIntent(Intent intent) {
+        return intent.getStringExtra(EXTRA_INPUT_ID);
+    }
+
+    private AudioOnlyTvServiceUtil() {}
+}
diff --git a/src/com/android/tv/audiotvservice/README.md b/src/com/android/tv/audiotvservice/README.md
new file mode 100644
index 0000000..0f40ff6
--- /dev/null
+++ b/src/com/android/tv/audiotvservice/README.md
@@ -0,0 +1,18 @@
+# AudioOnlyTvServiceUtil
+
+This service plays audio only TV inputs in the "background".
+
+
+
+## Usage
+
+To start playing call
+
+```java
+AudioOnlyTvServiceUtil.startAudioOnlyInput(context, tivInputServiceUri);
+```
+To stop the playback call.
+
+```java
+AudioOnlyTvServiceUtil.stopAudioOnlyInput(context);
+```
\ No newline at end of file
diff --git a/src/com/android/tv/data/BaseProgram.java b/src/com/android/tv/data/BaseProgram.java
index 0fb1e58..9650fd1 100644
--- a/src/com/android/tv/data/BaseProgram.java
+++ b/src/com/android/tv/data/BaseProgram.java
@@ -21,7 +21,9 @@
 import android.support.annotation.Nullable;
 import android.text.TextUtils;
 import com.android.tv.R;
+import com.google.common.collect.ImmutableList;
 import java.util.Comparator;
+import java.util.Objects;
 
 /**
  * Base class for {@link com.android.tv.data.Program} and {@link
@@ -43,6 +45,10 @@
     public static final Comparator<BaseProgram> SEASON_REVERSED_EPISODE_COMPARATOR =
             new EpisodeComparator(true);
 
+    public static final String COLUMN_SERIES_ID = "series_id";
+
+    public static final String COLUMN_STATE = "state";
+
     private static class EpisodeComparator implements Comparator<BaseProgram> {
         private final boolean mReversedSeason;
 
@@ -66,7 +72,7 @@
 
     /** Compares two strings represent season numbers or episode numbers of programs. */
     public static int numberCompare(String s1, String s2) {
-        if (s1 == s2) {
+        if (Objects.equals(s1, s2)) {
             return 0;
         } else if (s1 == null) {
             return -1;
@@ -92,6 +98,7 @@
     public abstract String getEpisodeTitle();
 
     /** Returns the displayed title of the program episode. */
+    @Nullable
     public String getEpisodeDisplayTitle(Context context) {
         String episodeNumber = getEpisodeNumber();
         String episodeTitle = getEpisodeTitle();
@@ -162,6 +169,7 @@
     public abstract long getDurationMillis();
 
     /** Returns the series ID. */
+    @Nullable
     public abstract String getSeriesId();
 
     /** Returns the season number. */
@@ -180,8 +188,7 @@
     public abstract int[] getCanonicalGenreIds();
 
     /** Returns the array of content ratings. */
-    @Nullable
-    public abstract TvContentRating[] getContentRatings();
+    public abstract ImmutableList<TvContentRating> getContentRatings();
 
     /** Returns channel's ID of the program. */
     public abstract long getChannelId();
diff --git a/src/com/android/tv/data/ChannelDataManager.java b/src/com/android/tv/data/ChannelDataManager.java
index 1dfcf12..a5c786c 100644
--- a/src/com/android/tv/data/ChannelDataManager.java
+++ b/src/com/android/tv/data/ChannelDataManager.java
@@ -23,7 +23,6 @@
 import android.content.SharedPreferences.Editor;
 import android.content.res.AssetFileDescriptor;
 import android.database.ContentObserver;
-import android.database.sqlite.SQLiteException;
 import android.media.tv.TvContract;
 import android.media.tv.TvContract.Channels;
 import android.media.tv.TvInputManager.TvInputCallback;
@@ -47,7 +46,7 @@
 import com.android.tv.util.AsyncDbTask;
 import com.android.tv.util.TvInputManagerHelper;
 import com.android.tv.util.Utils;
-import java.io.IOException;
+import java.io.FileNotFoundException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -515,7 +514,7 @@
         if (mChannelsUpdateTask != null) {
             mChannelsUpdateTask.cancel(true);
         }
-        mChannelsUpdateTask = new QueryAllChannelsTask(mContentResolver);
+        mChannelsUpdateTask = new QueryAllChannelsTask();
         mChannelsUpdateTask.executeOnDbThread();
     }
 
@@ -599,8 +598,10 @@
                             .openAssetFileDescriptor(
                                     TvContract.buildChannelLogoUri(mChannel.getId()), "r")) {
                 return true;
-            } catch (SQLiteException | IOException | NullPointerException e) {
-                // File not found or asset file not found.
+            } catch (FileNotFoundException e) {
+                // no need to log just return false
+            } catch (Exception e) {
+                Log.w(TAG, "Unable to find logo for " + mChannel, e);
             }
             return false;
         }
@@ -616,8 +617,8 @@
 
     private final class QueryAllChannelsTask extends AsyncDbTask.AsyncChannelQueryTask {
 
-        QueryAllChannelsTask(ContentResolver contentResolver) {
-            super(mDbExecutor, contentResolver);
+        QueryAllChannelsTask() {
+            super(mDbExecutor, mContext);
         }
 
         @Override
@@ -736,15 +737,12 @@
             return;
         }
         mDbExecutor.execute(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        String selection = Utils.buildSelectionForIds(Channels._ID, ids);
-                        ContentValues values = new ContentValues();
-                        values.put(columnName, columnValue);
-                        mContentResolver.update(
-                                TvContract.Channels.CONTENT_URI, values, selection, null);
-                    }
+                () -> {
+                    String selection = Utils.buildSelectionForIds(Channels._ID, ids);
+                    ContentValues values = new ContentValues();
+                    values.put(columnName, columnValue);
+                    mContentResolver.update(
+                            TvContract.Channels.CONTENT_URI, values, selection, null);
                 });
     }
 
diff --git a/src/com/android/tv/data/ChannelImpl.java b/src/com/android/tv/data/ChannelImpl.java
index 703f69c..f31290d 100644
--- a/src/com/android/tv/data/ChannelImpl.java
+++ b/src/com/android/tv/data/ChannelImpl.java
@@ -46,12 +46,8 @@
 
     /** Compares the channel numbers of channels which belong to the same input. */
     public static final Comparator<Channel> CHANNEL_NUMBER_COMPARATOR =
-            new Comparator<Channel>() {
-                @Override
-                public int compare(Channel lhs, Channel rhs) {
-                    return ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber());
-                }
-            };
+            (Channel lhs, Channel rhs) ->
+                    ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber());
 
     private static final int APP_LINK_TYPE_NOT_SET = 0;
     private static final String INVALID_PACKAGE_NAME = "packageName";
@@ -74,6 +70,7 @@
         TvContract.Channels.COLUMN_APP_LINK_ICON_URI,
         TvContract.Channels.COLUMN_APP_LINK_POSTER_ART_URI,
         TvContract.Channels.COLUMN_APP_LINK_INTENT_URI,
+        TvContract.Channels.COLUMN_NETWORK_AFFILIATION,
         TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, // Only used in bundled input
     };
 
@@ -102,6 +99,7 @@
         channel.mAppLinkIconUri = cursor.getString(index++);
         channel.mAppLinkPosterArtUri = cursor.getString(index++);
         channel.mAppLinkIntentUri = cursor.getString(index++);
+        channel.mNetworkAffiliation = cursor.getString(index++);
         if (CommonUtils.isBundledInput(channel.mInputId)) {
             channel.mRecordingProhibited = cursor.getInt(index++) != 0;
         }
@@ -146,6 +144,7 @@
     private String mAppLinkPosterArtUri;
     private String mAppLinkIntentUri;
     private Intent mAppLinkIntent;
+    private String mNetworkAffiliation;
     private int mAppLinkType;
     private String mLogoUri;
     private boolean mRecordingProhibited;
@@ -247,6 +246,11 @@
         return mAppLinkIntentUri;
     }
 
+    @Override
+    public String getNetworkAffiliation() {
+        return mNetworkAffiliation;
+    }
+
     /** Returns channel logo uri which is got from cloud, it's used only for ChannelLogoFetcher. */
     @Override
     public String getLogoUri() {
@@ -311,6 +315,11 @@
         mLogoUri = logoUri;
     }
 
+    @Override
+    public void setNetworkAffiliation(String networkAffiliation) {
+        mNetworkAffiliation = networkAffiliation;
+    }
+
     /**
      * Check whether {@code other} has same read-only channel info as this. But, it cannot check two
      * channels have same logos. It also excludes browsable and locked, because two fields are
@@ -393,8 +402,10 @@
             mAppLinkIconUri = channel.getAppLinkIconUri();
             mAppLinkPosterArtUri = channel.getAppLinkPosterArtUri();
             mAppLinkIntentUri = channel.getAppLinkIntentUri();
+            mNetworkAffiliation = channel.getNetworkAffiliation();
             mRecordingProhibited = channel.isRecordingProhibited();
             mChannelLogoExist = channel.channelLogoExists();
+            mNetworkAffiliation = channel.getNetworkAffiliation();
         }
     }
 
@@ -421,6 +432,7 @@
         mAppLinkIconUri = other.mAppLinkIconUri;
         mAppLinkPosterArtUri = other.mAppLinkPosterArtUri;
         mAppLinkIntentUri = other.mAppLinkIntentUri;
+        mNetworkAffiliation = channel.mNetworkAffiliation;
         mAppLinkIntent = other.mAppLinkIntent;
         mAppLinkType = other.mAppLinkType;
         mRecordingProhibited = other.mRecordingProhibited;
@@ -543,6 +555,12 @@
             return this;
         }
 
+        @VisibleForTesting
+        public Builder setNetworkAffiliation(String networkAffiliation) {
+            mChannel.mNetworkAffiliation = networkAffiliation;
+            return this;
+        }
+
         public Builder setAppLinkColor(int appLinkColor) {
             mChannel.mAppLinkColor = appLinkColor;
             return this;
diff --git a/src/com/android/tv/data/PreviewDataManager.java b/src/com/android/tv/data/PreviewDataManager.java
index 44664dc..8616aee 100644
--- a/src/com/android/tv/data/PreviewDataManager.java
+++ b/src/com/android/tv/data/PreviewDataManager.java
@@ -21,7 +21,6 @@
 import android.content.ContentUris;
 import android.content.Context;
 import android.database.Cursor;
-import android.database.SQLException;
 import android.graphics.Bitmap;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
@@ -31,10 +30,10 @@
 import android.os.Build;
 import android.support.annotation.IntDef;
 import android.support.annotation.MainThread;
-import android.support.media.tv.ChannelLogoUtils;
-import android.support.media.tv.PreviewProgram;
 import android.util.Log;
 import android.util.Pair;
+import androidx.tvprovider.media.tv.ChannelLogoUtils;
+import androidx.tvprovider.media.tv.PreviewProgram;
 import com.android.tv.R;
 import com.android.tv.common.util.PermissionUtils;
 import java.lang.annotation.Retention;
@@ -225,14 +224,14 @@
                     try (Cursor cursor =
                             mContentResolver.query(
                                     previewChannelsUri,
-                                    android.support.media.tv.Channel.PROJECTION,
+                                    androidx.tvprovider.media.tv.Channel.PROJECTION,
                                     mChannelSelection,
                                     new String[] {packageName},
                                     null)) {
                         if (cursor != null) {
                             while (cursor.moveToNext()) {
-                                android.support.media.tv.Channel previewChannel =
-                                        android.support.media.tv.Channel.fromCursor(cursor);
+                                androidx.tvprovider.media.tv.Channel previewChannel =
+                                        androidx.tvprovider.media.tv.Channel.fromCursor(cursor);
                                 Long previewChannelType = previewChannel.getInternalProviderFlag1();
                                 if (previewChannelType != null) {
                                     previewData.addPreviewChannelId(
@@ -245,14 +244,14 @@
                     try (Cursor cursor =
                             mContentResolver.query(
                                     previewChannelsUri,
-                                    android.support.media.tv.Channel.PROJECTION,
+                                    androidx.tvprovider.media.tv.Channel.PROJECTION,
                                     null,
                                     null,
                                     null)) {
                         if (cursor != null) {
                             while (cursor.moveToNext()) {
-                                android.support.media.tv.Channel previewChannel =
-                                        android.support.media.tv.Channel.fromCursor(cursor);
+                                androidx.tvprovider.media.tv.Channel previewChannel =
+                                        androidx.tvprovider.media.tv.Channel.fromCursor(cursor);
                                 Long previewChannelType = previewChannel.getInternalProviderFlag1();
                                 if (packageName.equals(previewChannel.getPackageName())
                                         && previewChannelType != null) {
@@ -283,7 +282,7 @@
                         }
                     }
                 }
-            } catch (SQLException e) {
+            } catch (Exception e) {
                 Log.w(TAG, "Unable to get preview data", e);
             }
             return previewData;
@@ -554,7 +553,7 @@
     /** A utils class for preview data. */
     public static final class PreviewDataUtils {
         /** Creates a preview channel. */
-        public static android.support.media.tv.Channel createPreviewChannel(
+        public static androidx.tvprovider.media.tv.Channel createPreviewChannel(
                 Context context, @PreviewChannelType long previewChannelType) {
             if (previewChannelType == TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL) {
                 return createRecordedProgramPreviewChannel(context, previewChannelType);
@@ -562,10 +561,10 @@
             return createDefaultPreviewChannel(context, previewChannelType);
         }
 
-        private static android.support.media.tv.Channel createDefaultPreviewChannel(
+        private static androidx.tvprovider.media.tv.Channel createDefaultPreviewChannel(
                 Context context, @PreviewChannelType long previewChannelType) {
-            android.support.media.tv.Channel.Builder builder =
-                    new android.support.media.tv.Channel.Builder();
+            androidx.tvprovider.media.tv.Channel.Builder builder =
+                    new androidx.tvprovider.media.tv.Channel.Builder();
             CharSequence appLabel =
                     context.getApplicationInfo().loadLabel(context.getPackageManager());
             CharSequence appDescription =
@@ -578,10 +577,10 @@
             return builder.build();
         }
 
-        private static android.support.media.tv.Channel createRecordedProgramPreviewChannel(
+        private static androidx.tvprovider.media.tv.Channel createRecordedProgramPreviewChannel(
                 Context context, @PreviewChannelType long previewChannelType) {
-            android.support.media.tv.Channel.Builder builder =
-                    new android.support.media.tv.Channel.Builder();
+            androidx.tvprovider.media.tv.Channel.Builder builder =
+                    new androidx.tvprovider.media.tv.Channel.Builder();
             builder.setType(TvContract.Channels.TYPE_PREVIEW)
                     .setDisplayName(
                             context.getResources()
diff --git a/src/com/android/tv/data/PreviewProgramContent.java b/src/com/android/tv/data/PreviewProgramContent.java
index b515640..8d4b88c 100644
--- a/src/com/android/tv/data/PreviewProgramContent.java
+++ b/src/com/android/tv/data/PreviewProgramContent.java
@@ -19,9 +19,9 @@
 import android.content.Context;
 import android.net.Uri;
 import android.support.annotation.VisibleForTesting;
-import android.support.media.tv.TvContractCompat;
 import android.text.TextUtils;
 import android.util.Pair;
+import androidx.tvprovider.media.tv.TvContractCompat;
 import com.android.tv.TvSingletons;
 import com.android.tv.data.api.Channel;
 import com.android.tv.dvr.data.RecordedProgram;
diff --git a/src/com/android/tv/data/Program.java b/src/com/android/tv/data/Program.java
index 2c64cdb..b688927 100644
--- a/src/com/android/tv/data/Program.java
+++ b/src/com/android/tv/data/Program.java
@@ -30,6 +30,7 @@
 import android.support.annotation.Nullable;
 import android.support.annotation.UiThread;
 import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
 import android.text.TextUtils;
 import android.util.Log;
 import com.android.tv.common.BuildConfig;
@@ -37,8 +38,10 @@
 import com.android.tv.common.util.CollectionUtils;
 import com.android.tv.common.util.CommonUtils;
 import com.android.tv.data.api.Channel;
+import com.android.tv.util.TvProviderUtils;
 import com.android.tv.util.Utils;
 import com.android.tv.util.images.ImageLoader;
+import com.google.common.collect.ImmutableList;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -86,6 +89,16 @@
 
     public static final String[] PROJECTION = createProjection();
 
+    public static final String[] PARTIAL_PROJECTION = {
+        TvContract.Programs._ID,
+        TvContract.Programs.COLUMN_CHANNEL_ID,
+        TvContract.Programs.COLUMN_TITLE,
+        TvContract.Programs.COLUMN_EPISODE_TITLE,
+        TvContract.Programs.COLUMN_CANONICAL_GENRE,
+        TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
+        TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
+    };
+
     private static String[] createProjection() {
         return CollectionUtils.concatAll(
                 PROJECTION_BASE,
@@ -94,7 +107,10 @@
                         : PROJECTION_DEPRECATED_IN_NYC);
     }
 
-    /** Returns the column index for {@code column}, -1 if the column doesn't exist. */
+    /**
+     * Returns the column index for {@code column},-1 if the column doesn't exist in {@link
+     * #PROJECTION}.
+     */
     public static int getColumnIndex(String column) {
         for (int i = 0; i < PROJECTION.length; ++i) {
             if (PROJECTION[i].equals(column)) {
@@ -104,11 +120,7 @@
         return -1;
     }
 
-    /**
-     * Creates {@code Program} object from cursor.
-     *
-     * <p>The query that created the cursor MUST use {@link #PROJECTION}.
-     */
+    /** Creates {@code Program} object from cursor. */
     public static Program fromCursor(Cursor cursor) {
         // Columns read must match the order of match {@link #PROJECTION}
         Builder builder = new Builder();
@@ -143,6 +155,27 @@
             builder.setSeasonNumber(cursor.getString(index++));
             builder.setEpisodeNumber(cursor.getString(index++));
         }
+        if (TvProviderUtils.getProgramHasSeriesIdColumn()) {
+            String seriesId = cursor.getString(index);
+            if (!TextUtils.isEmpty(seriesId)) {
+                builder.setSeriesId(seriesId);
+            }
+        }
+        return builder.build();
+    }
+
+    /** Creates {@code Program} object from cursor. */
+    public static Program fromCursorPartialProjection(Cursor cursor) {
+        // Columns read must match the order of match {@link #PARTIAL_PROJECTION}
+        Builder builder = new Builder();
+        int index = 0;
+        builder.setId(cursor.getLong(index++));
+        builder.setChannelId(cursor.getLong(index++));
+        builder.setTitle(cursor.getString(index++));
+        builder.setEpisodeTitle(cursor.getString(index++));
+        builder.setCanonicalGenres(cursor.getString(index++));
+        builder.setStartTimeUtcMillis(cursor.getLong(index++));
+        builder.setEndTimeUtcMillis(cursor.getLong(index++));
         return builder.build();
     }
 
@@ -169,10 +202,14 @@
         program.mCanonicalGenreIds = in.createIntArray();
         int length = in.readInt();
         if (length > 0) {
-            program.mContentRatings = new TvContentRating[length];
+            ImmutableList.Builder<TvContentRating> ratingsBuilder =
+                    ImmutableList.builderWithExpectedSize(length);
             for (int i = 0; i < length; ++i) {
-                program.mContentRatings[i] = TvContentRating.unflattenFromString(in.readString());
+                ratingsBuilder.add(TvContentRating.unflattenFromString(in.readString()));
             }
+            program.mContentRatings = ratingsBuilder.build();
+        } else {
+            program.mContentRatings = ImmutableList.of();
         }
         program.mRecordingProhibited = in.readByte() != (byte) 0;
         return program;
@@ -202,6 +239,7 @@
     private String mEpisodeNumber;
     private long mStartTimeUtcMillis;
     private long mEndTimeUtcMillis;
+    private String mDurationString;
     private String mDescription;
     private String mLongDescription;
     private int mVideoWidth;
@@ -210,7 +248,7 @@
     private String mPosterArtUri;
     private String mThumbnailUri;
     private int[] mCanonicalGenreIds;
-    private TvContentRating[] mContentRatings;
+    private ImmutableList<TvContentRating> mContentRatings;
     private boolean mRecordingProhibited;
 
     private Program() {
@@ -278,6 +316,15 @@
         return mEndTimeUtcMillis;
     }
 
+    public String getDurationString(Context context) {
+        // TODO(b/71717446): expire the calculated string
+        if (mDurationString == null) {
+            mDurationString =
+                    Utils.getDurationString(context, mStartTimeUtcMillis, mEndTimeUtcMillis, true);
+        }
+        return mDurationString;
+    }
+
     /** Returns the program duration. */
     @Override
     public long getDurationMillis() {
@@ -310,7 +357,7 @@
 
     @Nullable
     @Override
-    public TvContentRating[] getContentRatings() {
+    public ImmutableList<TvContentRating> getContentRatings() {
         return mContentRatings;
     }
 
@@ -379,7 +426,7 @@
                 mVideoHeight,
                 mPosterArtUri,
                 mThumbnailUri,
-                Arrays.hashCode(mContentRatings),
+                mContentRatings,
                 Arrays.hashCode(mCanonicalGenreIds),
                 mSeasonNumber,
                 mSeasonTitle,
@@ -407,7 +454,7 @@
                 && mVideoHeight == program.mVideoHeight
                 && Objects.equals(mPosterArtUri, program.mPosterArtUri)
                 && Objects.equals(mThumbnailUri, program.mThumbnailUri)
-                && Arrays.equals(mContentRatings, program.mContentRatings)
+                && Objects.equals(mContentRatings, program.mContentRatings)
                 && Arrays.equals(mCanonicalGenreIds, program.mCanonicalGenreIds)
                 && Objects.equals(mSeasonNumber, program.mSeasonNumber)
                 && Objects.equals(mSeasonTitle, program.mSeasonTitle)
@@ -474,7 +521,8 @@
      */
     @SuppressLint("InlinedApi")
     @SuppressWarnings("deprecation")
-    public static ContentValues toContentValues(Program program) {
+    @WorkerThread
+    public static ContentValues toContentValues(Program program, Context context) {
         ContentValues values = new ContentValues();
         values.put(TvContract.Programs.COLUMN_CHANNEL_ID, program.getChannelId());
         if (!TextUtils.isEmpty(program.getPackageName())) {
@@ -495,6 +543,10 @@
             putValue(values, TvContract.Programs.COLUMN_SEASON_NUMBER, program.getSeasonNumber());
             putValue(values, TvContract.Programs.COLUMN_EPISODE_NUMBER, program.getEpisodeNumber());
         }
+        if (TvProviderUtils.checkSeriesIdColumn(context, Programs.CONTENT_URI)) {
+            putValue(values, COLUMN_SERIES_ID, program.getSeriesId());
+        }
+
         putValue(values, TvContract.Programs.COLUMN_SHORT_DESCRIPTION, program.getDescription());
         putValue(values, TvContract.Programs.COLUMN_LONG_DESCRIPTION, program.getLongDescription());
         putValue(values, TvContract.Programs.COLUMN_POSTER_ART_URI, program.getPosterArtUri());
@@ -554,6 +606,7 @@
         mEpisodeNumber = other.mEpisodeNumber;
         mStartTimeUtcMillis = other.mStartTimeUtcMillis;
         mEndTimeUtcMillis = other.mEndTimeUtcMillis;
+        mDurationString = null; // Recreate Duration when needed.
         mDescription = other.mDescription;
         mLongDescription = other.mLongDescription;
         mVideoWidth = other.mVideoWidth;
@@ -582,6 +635,7 @@
             mProgram.mEpisodeNumber = null;
             mProgram.mStartTimeUtcMillis = -1;
             mProgram.mEndTimeUtcMillis = -1;
+            mProgram.mDurationString = null;
             mProgram.mDescription = null;
             mProgram.mLongDescription = null;
             mProgram.mRecordingProhibited = false;
@@ -771,7 +825,7 @@
          * @param contentRatings the content ratings
          * @return a reference to this object
          */
-        public Builder setContentRatings(TvContentRating[] contentRatings) {
+        public Builder setContentRatings(ImmutableList<TvContentRating> contentRatings) {
             mProgram.mContentRatings = contentRatings;
             return this;
         }
@@ -947,7 +1001,7 @@
         out.writeString(mPosterArtUri);
         out.writeString(mThumbnailUri);
         out.writeIntArray(mCanonicalGenreIds);
-        out.writeInt(mContentRatings == null ? 0 : mContentRatings.length);
+        out.writeInt(mContentRatings == null ? 0 : mContentRatings.size());
         if (mContentRatings != null) {
             for (TvContentRating rating : mContentRatings) {
                 out.writeString(rating.flattenToString());
diff --git a/src/com/android/tv/data/ProgramDataManager.java b/src/com/android/tv/data/ProgramDataManager.java
index 4631806..2f20c89 100644
--- a/src/com/android/tv/data/ProgramDataManager.java
+++ b/src/com/android/tv/data/ProgramDataManager.java
@@ -35,14 +35,17 @@
 import android.util.LruCache;
 import com.android.tv.TvSingletons;
 import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.config.api.RemoteConfig;
-import com.android.tv.common.config.api.RemoteConfigValue;
 import com.android.tv.common.memory.MemoryManageable;
 import com.android.tv.common.util.Clock;
 import com.android.tv.data.api.Channel;
+import com.android.tv.perf.EventNames;
+import com.android.tv.perf.PerformanceMonitor;
+import com.android.tv.perf.TimerEvent;
 import com.android.tv.util.AsyncDbTask;
 import com.android.tv.util.MultiLongSparseArray;
+import com.android.tv.util.TvProviderUtils;
 import com.android.tv.util.Utils;
+import com.android.tv.common.flags.BackendKnobsFlags;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -71,8 +74,6 @@
     // TODO: need to optimize consecutive DB updates.
     private static final long CURRENT_PROGRAM_UPDATE_WAIT_MS = TimeUnit.SECONDS.toMillis(5);
     @VisibleForTesting static final long PROGRAM_GUIDE_SNAP_TIME_MS = TimeUnit.MINUTES.toMillis(30);
-    private static final RemoteConfigValue<Long> PROGRAM_GUIDE_MAX_HOURS =
-            RemoteConfigValue.create("live_channels_program_guide_max_hours", 48);
 
     // TODO: Use TvContract constants, once they become public.
     private static final String PARAM_START_TIME = "start_time";
@@ -90,10 +91,13 @@
     private static final int MSG_UPDATE_ONE_CURRENT_PROGRAM = 1001;
     private static final int MSG_UPDATE_PREFETCH_PROGRAM = 1002;
 
+    private final Context mContext;
     private final Clock mClock;
     private final ContentResolver mContentResolver;
     private final Executor mDbExecutor;
-    private final RemoteConfig mRemoteConfig;
+    private final BackendKnobsFlags mBackendKnobsFlags;
+    private final PerformanceMonitor mPerformanceMonitor;
+    private final ChannelDataManager mChannelDataManager;
     private boolean mStarted;
     // Updated only on the main thread.
     private volatile boolean mCurrentProgramsLoadFinished;
@@ -104,15 +108,15 @@
     private final MultiLongSparseArray<OnCurrentProgramUpdatedListener>
             mChannelId2ProgramUpdatedListeners = new MultiLongSparseArray<>();
     private final Handler mHandler;
-    private final Set<Listener> mListeners = new ArraySet<>();
-
+    private final Set<Callback> mCallbacks = new ArraySet<>();
+    private Map<Long, ArrayList<Program>> mChannelIdProgramCache = new ConcurrentHashMap<>();
+    private final Set<Long> mCompleteInfoChannelIds = new HashSet<>();
     private final ContentObserver mProgramObserver;
 
     private boolean mPrefetchEnabled;
     private long mProgramPrefetchUpdateWaitMs;
     private long mLastPrefetchTaskRunMs;
     private ProgramsPrefetchTask mProgramsPrefetchTask;
-    private Map<Long, ArrayList<Program>> mChannelIdProgramCache = new HashMap<>();
 
     // Any program that ends prior to this time will be removed from the cache
     // when a channel's current program is updated.
@@ -125,25 +129,34 @@
     @MainThread
     public ProgramDataManager(Context context) {
         this(
+                context,
                 TvSingletons.getSingletons(context).getDbExecutor(),
                 context.getContentResolver(),
                 Clock.SYSTEM,
                 Looper.myLooper(),
-                TvSingletons.getSingletons(context).getRemoteConfig());
+                TvSingletons.getSingletons(context).getBackendKnobs(),
+                TvSingletons.getSingletons(context).getPerformanceMonitor(),
+                TvSingletons.getSingletons(context).getChannelDataManager());
     }
 
     @VisibleForTesting
     ProgramDataManager(
+            Context context,
             Executor executor,
             ContentResolver contentResolver,
             Clock time,
             Looper looper,
-            RemoteConfig remoteConfig) {
+            BackendKnobsFlags backendKnobsFlags,
+            PerformanceMonitor performanceMonitor,
+            ChannelDataManager channelDataManager) {
+        mContext = context;
         mDbExecutor = executor;
         mClock = time;
         mContentResolver = contentResolver;
         mHandler = new MyHandler(looper);
-        mRemoteConfig = remoteConfig;
+        mBackendKnobsFlags = backendKnobsFlags;
+        mPerformanceMonitor = performanceMonitor;
+        mChannelDataManager = channelDataManager;
         mProgramObserver =
                 new ContentObserver(mHandler) {
                     @Override
@@ -246,24 +259,43 @@
         }
     }
 
-    /** A listener interface to receive notification on program data retrieval from DB. */
-    public interface Listener {
+    public void prefetchChannel(long channelId) {
+        if (mCompleteInfoChannelIds.add(channelId)) {
+            long startTimeMs =
+                    Utils.floorTime(
+                            mClock.currentTimeMillis() - PROGRAM_GUIDE_SNAP_TIME_MS,
+                            PROGRAM_GUIDE_SNAP_TIME_MS);
+            long endTimeMs = startTimeMs + TimeUnit.HOURS.toMillis(getFetchDuration());
+            new SingleChannelPrefetchTask(channelId, startTimeMs, endTimeMs).executeOnDbThread();
+        }
+    }
+
+    /** A Callback interface to receive notification on program data retrieval from DB. */
+    public interface Callback {
         /**
          * Called when a Program data is now available through getProgram() after the DB operation
          * is done which wasn't before. This would be called only if fetched data is around the
          * selected program.
          */
         void onProgramUpdated();
+
+        /**
+         * Called when we update complete program data of specific channel during scrolling. Data is
+         * loaded from DB on request basis.
+         *
+         * @param channelId
+         */
+        void onSingleChannelUpdated(long channelId);
     }
 
-    /** Adds the {@link Listener}. */
-    public void addListener(Listener listener) {
-        mListeners.add(listener);
+    /** Adds the {@link Callback}. */
+    public void addCallback(Callback callback) {
+        mCallbacks.add(callback);
     }
 
-    /** Removes the {@link Listener}. */
-    public void removeListener(Listener listener) {
-        mListeners.remove(listener);
+    /** Removes the {@link Callback}. */
+    public void removeCallback(Callback callback) {
+        mCallbacks.remove(callback);
     }
 
     /** Enables or Disables program prefetch. */
@@ -451,7 +483,7 @@
         }
         clearTask(mProgramUpdateTaskMap);
         mHandler.removeMessages(MSG_UPDATE_ONE_CURRENT_PROGRAM);
-        mProgramsUpdateTask = new ProgramsUpdateTask(mContentResolver, mClock.currentTimeMillis());
+        mProgramsUpdateTask = new ProgramsUpdateTask(mClock.currentTimeMillis());
         mProgramsUpdateTask.executeOnDbThread();
     }
 
@@ -461,20 +493,29 @@
         private final long mEndTimeMs;
 
         private boolean mSuccess;
+        private TimerEvent mFromEmptyCacheTimeEvent;
 
         public ProgramsPrefetchTask() {
             super(mDbExecutor);
             long time = mClock.currentTimeMillis();
             mStartTimeMs =
                     Utils.floorTime(time - PROGRAM_GUIDE_SNAP_TIME_MS, PROGRAM_GUIDE_SNAP_TIME_MS);
-            mEndTimeMs =
-                    mStartTimeMs
-                            + TimeUnit.HOURS.toMillis(PROGRAM_GUIDE_MAX_HOURS.get(mRemoteConfig));
+            mEndTimeMs = mStartTimeMs + TimeUnit.HOURS.toMillis(getFetchDuration());
             mSuccess = false;
         }
 
         @Override
+        protected void onPreExecute() {
+            if (mChannelIdCurrentProgramMap.isEmpty()) {
+                // No current program guide is shown.
+                // Measure the delay before users can see program guides.
+                mFromEmptyCacheTimeEvent = mPerformanceMonitor.startTimer();
+            }
+        }
+
+        @Override
         protected Map<Long, ArrayList<Program>> doInBackground(Void... params) {
+            TimerEvent asyncTimeEvent = mPerformanceMonitor.startTimer();
             Map<Long, ArrayList<Program>> programMap = new HashMap<>();
             if (DEBUG) {
                 Log.d(
@@ -497,8 +538,19 @@
                     return null;
                 }
                 programMap.clear();
-                try (Cursor c =
-                        mContentResolver.query(uri, Program.PROJECTION, null, null, SORT_BY_TIME)) {
+
+                String[] projection =
+                        mBackendKnobsFlags.enablePartialProgramFetch()
+                                ? Program.PARTIAL_PROJECTION
+                                : Program.PROJECTION;
+                if (TvProviderUtils.checkSeriesIdColumn(mContext, Programs.CONTENT_URI)) {
+                    if (Utils.isProgramsUri(uri)) {
+                        projection =
+                                TvProviderUtils.addExtraColumnsToProjection(
+                                        projection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID);
+                    }
+                }
+                try (Cursor c = mContentResolver.query(uri, projection, null, null, SORT_BY_TIME)) {
                     if (c == null) {
                         continue;
                     }
@@ -510,7 +562,10 @@
                             }
                             return null;
                         }
-                        Program program = Program.fromCursor(c);
+                        Program program =
+                                mBackendKnobsFlags.enablePartialProgramFetch()
+                                        ? Program.fromCursorPartialProjection(c)
+                                        : Program.fromCursor(c);
                         if (Program.isDuplicate(program, lastReadProgram)) {
                             duplicateCount++;
                             continue;
@@ -520,6 +575,15 @@
                         ArrayList<Program> programs = programMap.get(program.getChannelId());
                         if (programs == null) {
                             programs = new ArrayList<>();
+                            if (mBackendKnobsFlags.enablePartialProgramFetch()) {
+                                // To skip already loaded complete data.
+                                Program currentProgramInfo =
+                                        mChannelIdCurrentProgramMap.get(program.getChannelId());
+                                if (currentProgramInfo != null
+                                        && Program.isDuplicate(program, currentProgramInfo)) {
+                                    program = currentProgramInfo;
+                                }
+                            }
                             programMap.put(program.getChannelId(), programs);
                         }
                         programs.add(program);
@@ -534,12 +598,17 @@
                         Log.d(TAG, "Database is changed while querying. Will retry.");
                     }
                 } catch (SecurityException e) {
-                    Log.d(TAG, "Security exception during program data query", e);
+                    Log.w(TAG, "Security exception during program data query", e);
+                } catch (Exception e) {
+                    Log.w(TAG, "Error during program data query", e);
                 }
             }
             if (DEBUG) {
                 Log.d(TAG, "Ends programs prefetch for " + programMap.size() + " channels");
             }
+            mPerformanceMonitor.stopTimer(
+                    asyncTimeEvent,
+                    EventNames.PROGRAM_DATA_MANAGER_PROGRAMS_PREFETCH_TASK_DO_IN_BACKGROUND);
             return programMap;
         }
 
@@ -552,8 +621,6 @@
             }
             long nextMessageDelayedTime;
             if (mSuccess) {
-                mChannelIdProgramCache = programs;
-                notifyProgramUpdated();
                 long currentTime = mClock.currentTimeMillis();
                 mLastPrefetchTaskRunMs = currentTime;
                 nextMessageDelayedTime =
@@ -561,6 +628,22 @@
                                         mLastPrefetchTaskRunMs + PROGRAM_GUIDE_SNAP_TIME_MS,
                                         PROGRAM_GUIDE_SNAP_TIME_MS)
                                 - currentTime;
+                // Issue second pre-fetch immediately after the first partial update
+                if (mChannelIdProgramCache.isEmpty()) {
+                    nextMessageDelayedTime = 0;
+                }
+                mChannelIdProgramCache = programs;
+                if (mBackendKnobsFlags.enablePartialProgramFetch()) {
+                    // Since cache has partial data we need to reset the map of complete data.
+                    mCompleteInfoChannelIds.clear();
+                }
+                notifyProgramUpdated();
+                if (mFromEmptyCacheTimeEvent != null) {
+                    mPerformanceMonitor.stopTimer(
+                            mFromEmptyCacheTimeEvent,
+                            EventNames.PROGRAM_GUIDE_SHOW_FROM_EMPTY_CACHE);
+                    mFromEmptyCacheTimeEvent = null;
+                }
             } else {
                 nextMessageDelayedTime = PERIODIC_PROGRAM_UPDATE_MIN_MS;
             }
@@ -571,17 +654,78 @@
         }
     }
 
+    private long getFetchDuration() {
+        if (mChannelIdProgramCache.isEmpty()) {
+            return Math.max(1L, mBackendKnobsFlags.programGuideInitialFetchHours());
+        } else {
+            long durationHours;
+            int channelCount = mChannelDataManager.getChannelCount();
+            long knobsMaxHours = mBackendKnobsFlags.programGuideMaxHours();
+            long targetChannelCount = mBackendKnobsFlags.epgTargetChannelCount();
+            if (channelCount <= targetChannelCount) {
+                durationHours = Math.max(48L, knobsMaxHours);
+            } else {
+                // 2 days <= duration <= 14 days (336 hours)
+                durationHours = knobsMaxHours * targetChannelCount / channelCount;
+                if (durationHours < 48L) {
+                    durationHours = 48L;
+                } else if (durationHours > 336L) {
+                    durationHours = 336L;
+                }
+            }
+            return durationHours;
+        }
+    }
+
+    private class SingleChannelPrefetchTask extends AsyncDbTask.AsyncQueryTask<ArrayList<Program>> {
+        long mChannelId;
+
+        public SingleChannelPrefetchTask(long channelId, long startTimeMs, long endTimeMs) {
+            super(
+                    mDbExecutor,
+                    mContext,
+                    TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs),
+                    Program.PROJECTION,
+                    null,
+                    null,
+                    SORT_BY_TIME);
+            mChannelId = channelId;
+        }
+
+        @Override
+        protected ArrayList<Program> onQuery(Cursor c) {
+            ArrayList<Program> programMap = new ArrayList<>();
+            while (c.moveToNext()) {
+                Program program = Program.fromCursor(c);
+                programMap.add(program);
+            }
+            return programMap;
+        }
+
+        @Override
+        protected void onPostExecute(ArrayList<Program> programs) {
+            mChannelIdProgramCache.put(mChannelId, programs);
+            notifySingleChannelUpdated(mChannelId);
+        }
+    }
+
     private void notifyProgramUpdated() {
-        for (Listener listener : mListeners) {
-            listener.onProgramUpdated();
+        for (Callback callback : mCallbacks) {
+            callback.onProgramUpdated();
+        }
+    }
+
+    private void notifySingleChannelUpdated(long channelId) {
+        for (Callback callback : mCallbacks) {
+            callback.onSingleChannelUpdated(channelId);
         }
     }
 
     private class ProgramsUpdateTask extends AsyncDbTask.AsyncQueryTask<List<Program>> {
-        public ProgramsUpdateTask(ContentResolver contentResolver, long time) {
+        public ProgramsUpdateTask(long time) {
             super(
                     mDbExecutor,
-                    contentResolver,
+                    mContext,
                     Programs.CONTENT_URI
                             .buildUpon()
                             .appendQueryParameter(PARAM_START_TIME, String.valueOf(time))
@@ -633,6 +777,9 @@
                 for (Long channelId : removedChannelIds) {
                     if (mPrefetchEnabled) {
                         mChannelIdProgramCache.remove(channelId);
+                        if (mBackendKnobsFlags.enablePartialProgramFetch()) {
+                            mCompleteInfoChannelIds.remove(channelId);
+                        }
                     }
                     mChannelIdCurrentProgramMap.remove(channelId);
                     notifyCurrentProgramUpdate(channelId, null);
@@ -645,11 +792,10 @@
     private class UpdateCurrentProgramForChannelTask extends AsyncDbTask.AsyncQueryTask<Program> {
         private final long mChannelId;
 
-        private UpdateCurrentProgramForChannelTask(
-                ContentResolver contentResolver, long channelId, long time) {
+        private UpdateCurrentProgramForChannelTask(long channelId, long time) {
             super(
                     mDbExecutor,
-                    contentResolver,
+                    mContext,
                     TvContract.buildProgramsUriForChannel(channelId, time, time),
                     Program.PROJECTION,
                     null,
@@ -695,7 +841,7 @@
                         }
                         UpdateCurrentProgramForChannelTask task =
                                 new UpdateCurrentProgramForChannelTask(
-                                        mContentResolver, channelId, mClock.currentTimeMillis());
+                                        channelId, mClock.currentTimeMillis());
                         mProgramUpdateTaskMap.put(channelId, task);
                         task.executeOnDbThread();
                         break;
diff --git a/src/com/android/tv/data/WatchedHistoryManager.java b/src/com/android/tv/data/WatchedHistoryManager.java
index 7187efd..9c1d423 100644
--- a/src/com/android/tv/data/WatchedHistoryManager.java
+++ b/src/com/android/tv/data/WatchedHistoryManager.java
@@ -34,6 +34,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Scanner;
+import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -73,24 +74,20 @@
                         // onNewRecordAdded will be called in the same thread as the thread
                         // which created this instance.
                         mHandler.post(
-                                new Runnable() {
-                                    @Override
-                                    public void run() {
-                                        for (long i = mLastIndex + 1; i <= lastIndex; ++i) {
-                                            WatchedRecord record =
-                                                    decode(
-                                                            mSharedPreferences.getString(
-                                                                    getSharedPreferencesKey(i),
-                                                                    null));
-                                            if (record != null) {
-                                                mWatchedHistory.add(record);
-                                                if (mListener != null) {
-                                                    mListener.onNewRecordAdded(record);
-                                                }
+                                () -> {
+                                    for (long i = mLastIndex + 1; i <= lastIndex; ++i) {
+                                        WatchedRecord record =
+                                                decode(
+                                                        mSharedPreferences.getString(
+                                                                getSharedPreferencesKey(i), null));
+                                        if (record != null) {
+                                            mWatchedHistory.add(record);
+                                            if (mListener != null) {
+                                                mListener.onNewRecordAdded(record);
                                             }
                                         }
-                                        mLastIndex = lastIndex;
                                     }
+                                    mLastIndex = lastIndex;
                                 });
                     }
                 }
@@ -100,16 +97,18 @@
     private Listener mListener;
     private final int mMaxHistorySize;
     private final Handler mHandler;
+    private final Executor mExecutor;
 
     public WatchedHistoryManager(Context context) {
-        this(context, MAX_HISTORY_SIZE);
+        this(context, MAX_HISTORY_SIZE, AsyncTask.THREAD_POOL_EXECUTOR);
     }
 
     @VisibleForTesting
-    WatchedHistoryManager(Context context, int maxHistorySize) {
+    WatchedHistoryManager(Context context, int maxHistorySize, Executor executor) {
         mContext = context.getApplicationContext();
         mMaxHistorySize = maxHistorySize;
         mHandler = new Handler();
+        mExecutor = executor;
     }
 
     /** Starts the manager. It loads history data from {@link SharedPreferences}. */
@@ -130,7 +129,7 @@
                 protected void onPostExecute(Void params) {
                     onLoadFinished();
                 }
-            }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+            }.executeOnExecutor(mExecutor);
         } else {
             loadWatchedHistory();
             onLoadFinished();
diff --git a/src/com/android/tv/data/api/Channel.java b/src/com/android/tv/data/api/Channel.java
index 496331c..fb00952 100644
--- a/src/com/android/tv/data/api/Channel.java
+++ b/src/com/android/tv/data/api/Channel.java
@@ -85,6 +85,8 @@
 
     String getAppLinkIntentUri();
 
+    String getNetworkAffiliation();
+
     String getLogoUri();
 
     boolean isRecordingProhibited();
@@ -109,6 +111,8 @@
 
     void setLogoUri(String logoUri);
 
+    void setNetworkAffiliation(String networkAffiliation);
+
     boolean channelLogoExists();
 
     void loadBitmap(
diff --git a/src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java b/src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java
deleted file mode 100644
index 795ad5c..0000000
--- a/src/com/android/tv/data/epg/AutoValue_EpgReader_EpgChannel.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.tv.data.epg;
-
-import com.android.tv.data.api.Channel;
-
-/**
- * Hand copy of generated Autovalue class.
- *
- * TODO get autovalue working
- */
-final class AutoValue_EpgReader_EpgChannel extends EpgReader.EpgChannel {
-
-    private final Channel channel;
-    private final String epgChannelId;
-
-    AutoValue_EpgReader_EpgChannel(
-            Channel channel,
-            String epgChannelId) {
-        if (channel == null) {
-            throw new NullPointerException("Null channel");
-        }
-        this.channel = channel;
-        if (epgChannelId == null) {
-            throw new NullPointerException("Null epgChannelId");
-        }
-        this.epgChannelId = epgChannelId;
-    }
-
-    @Override
-    public Channel getChannel() {
-        return channel;
-    }
-
-    @Override
-    public String getEpgChannelId() {
-        return epgChannelId;
-    }
-
-    @Override
-    public String toString() {
-        return "EpgChannel{"
-                + "channel=" + channel + ", "
-                + "epgChannelId=" + epgChannelId
-                + "}";
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (o == this) {
-            return true;
-        }
-        if (o instanceof EpgReader.EpgChannel) {
-            EpgReader.EpgChannel that = (EpgReader.EpgChannel) o;
-            return (this.channel.equals(that.getChannel()))
-                    && (this.epgChannelId.equals(that.getEpgChannelId()));
-        }
-        return false;
-    }
-
-    @Override
-    public int hashCode() {
-        int h = 1;
-        h *= 1000003;
-        h ^= this.channel.hashCode();
-        h *= 1000003;
-        h ^= this.epgChannelId.hashCode();
-        return h;
-    }
-
-}
-
diff --git a/src/com/android/tv/data/epg/EpgFetchHelper.java b/src/com/android/tv/data/epg/EpgFetchHelper.java
index 3c7112e..3843ca9 100644
--- a/src/com/android/tv/data/epg/EpgFetchHelper.java
+++ b/src/com/android/tv/data/epg/EpgFetchHelper.java
@@ -17,6 +17,7 @@
 package com.android.tv.data.epg;
 
 import android.content.ContentProviderOperation;
+import android.content.ContentValues;
 import android.content.Context;
 import android.content.OperationApplicationException;
 import android.database.Cursor;
@@ -30,9 +31,13 @@
 import com.android.tv.common.CommonConstants;
 import com.android.tv.common.util.Clock;
 import com.android.tv.data.Program;
+import com.android.tv.data.api.Channel;
+import com.android.tv.features.TvFeatures;
+import com.android.tv.util.TvProviderUtils;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
 /** The helper class for {@link EpgFetcher} */
@@ -101,7 +106,7 @@
                     ops.add(
                             ContentProviderOperation.newUpdate(
                                             TvContract.buildProgramUri(oldProgram.getId()))
-                                    .withValues(Program.toContentValues(newProgram))
+                                    .withValues(Program.toContentValues(newProgram, context))
                                     .build());
                     oldProgramsIndex++;
                     newProgramsIndex++;
@@ -127,7 +132,7 @@
             if (addNewProgram) {
                 ops.add(
                         ContentProviderOperation.newInsert(Programs.CONTENT_URI)
-                                .withValues(Program.toContentValues(newProgram))
+                                .withValues(Program.toContentValues(newProgram, context))
                                 .build());
             }
             // Throttle the batch operation not to cause TransactionTooLargeException.
@@ -155,14 +160,57 @@
         return updated;
     }
 
+    @WorkerThread
+    static void updateNetworkAffiliation(Context context, Set<EpgReader.EpgChannel> channels) {
+        if (!TvFeatures.STORE_NETWORK_AFFILIATION.isEnabled(context)) {
+            return;
+        }
+        ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+        for (EpgReader.EpgChannel epgChannel : channels) {
+            if (!epgChannel.getDbUpdateNeeded()) {
+                continue;
+            }
+            Channel channel = epgChannel.getChannel();
+
+            ContentValues values = new ContentValues();
+            values.put(
+                    TvContract.Channels.COLUMN_NETWORK_AFFILIATION,
+                    channel.getNetworkAffiliation());
+            ops.add(
+                    ContentProviderOperation.newUpdate(TvContract.buildChannelUri(channel.getId()))
+                            .withValues(values)
+                            .build());
+            if (ops.size() >= BATCH_OPERATION_COUNT) {
+                try {
+                    context.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
+                } catch (RemoteException | OperationApplicationException e) {
+                    Log.e(TAG, "Failed to update channels.", e);
+                }
+                ops.clear();
+            }
+        }
+        try {
+            context.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
+        } catch (RemoteException | OperationApplicationException e) {
+            Log.e(TAG, "Failed to update channels.", e);
+        }
+    }
+
+    @WorkerThread
     private static List<Program> queryPrograms(
             Context context, long channelId, long startTimeMs, long endTimeMs) {
+        String[] projection = Program.PROJECTION;
+        if (TvProviderUtils.checkSeriesIdColumn(context, Programs.CONTENT_URI)) {
+            projection =
+                    TvProviderUtils.addExtraColumnsToProjection(
+                            projection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID);
+        }
         try (Cursor c =
                 context.getContentResolver()
                         .query(
                                 TvContract.buildProgramsUriForChannel(
                                         channelId, startTimeMs, endTimeMs),
-                                Program.PROJECTION,
+                                projection,
                                 null,
                                 null,
                                 Programs.COLUMN_START_TIME_UTC_MILLIS)) {
diff --git a/src/com/android/tv/data/epg/EpgFetcherImpl.java b/src/com/android/tv/data/epg/EpgFetcherImpl.java
index 2aaaa5b..b191421 100644
--- a/src/com/android/tv/data/epg/EpgFetcherImpl.java
+++ b/src/com/android/tv/data/epg/EpgFetcherImpl.java
@@ -38,11 +38,10 @@
 import android.support.annotation.WorkerThread;
 import android.text.TextUtils;
 import android.util.Log;
-import com.android.tv.TvFeatures;
 import com.android.tv.TvSingletons;
 import com.android.tv.common.BuildConfig;
 import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.config.api.RemoteConfigValue;
+import com.android.tv.common.buildtype.HasBuildType;
 import com.android.tv.common.util.Clock;
 import com.android.tv.common.util.CommonUtils;
 import com.android.tv.common.util.LocationUtils;
@@ -55,12 +54,15 @@
 import com.android.tv.data.Lineup;
 import com.android.tv.data.Program;
 import com.android.tv.data.api.Channel;
+import com.android.tv.features.TvFeatures;
 import com.android.tv.perf.EventNames;
 import com.android.tv.perf.PerformanceMonitor;
 import com.android.tv.perf.TimerEvent;
 import com.android.tv.util.Utils;
 import com.google.android.tv.partner.support.EpgInput;
 import com.google.android.tv.partner.support.EpgInputs;
+import com.google.common.collect.ImmutableSet;
+import com.android.tv.common.flags.BackendKnobsFlags;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -100,8 +102,7 @@
     private static final long FETCH_DURING_SCAN_DURATION_SEC = TimeUnit.HOURS.toSeconds(3);
     private static final long FAST_FETCH_DURATION_SEC = TimeUnit.DAYS.toSeconds(2);
 
-    private static final RemoteConfigValue<Long> ROUTINE_INTERVAL_HOUR =
-            RemoteConfigValue.create("live_channels_epg_fetcher_interval_hour", 4);
+    private static final long DEFAULT_ROUTINE_INTERVAL_HOUR = 4;
 
     private static final int MSG_PREPARE_FETCH_DURING_SCAN = 1;
     private static final int MSG_CHANNEL_UPDATED_DURING_SCAN = 2;
@@ -115,6 +116,9 @@
     private final ChannelDataManager mChannelDataManager;
     private final EpgReader mEpgReader;
     private final PerformanceMonitor mPerformanceMonitor;
+    private final EpgInputWhiteList mEpgInputWhiteList;
+    private final BackendKnobsFlags mBackendKnobsFlags;
+    private final HasBuildType.BuildType mBuildType;
     private FetchAsyncTask mFetchTask;
     private FetchDuringScanHandler mFetchDuringScanHandler;
     private long mEpgTimeStamp;
@@ -124,9 +128,6 @@
     // A flag to block the re-entrance of onChannelScanStarted and onChannelScanFinished.
     private boolean mScanStarted;
 
-    private final long mRoutineIntervalMs;
-    private final long mEpgDataExpiredTimeLimitMs;
-    private final long mFastFetchDurationSec;
     private Clock mClock;
 
     public static EpgFetcher create(Context context) {
@@ -136,36 +137,54 @@
         PerformanceMonitor performanceMonitor = tvSingletons.getPerformanceMonitor();
         EpgReader epgReader = tvSingletons.providesEpgReader().get();
         Clock clock = tvSingletons.getClock();
-        long routineIntervalMs = ROUTINE_INTERVAL_HOUR.get(tvSingletons.getRemoteConfig());
-
+        EpgInputWhiteList epgInputWhiteList =
+                new EpgInputWhiteList(tvSingletons.getCloudEpgFlags());
+        BackendKnobsFlags backendKnobsFlags = tvSingletons.getBackendKnobs();
+        HasBuildType.BuildType buildType = tvSingletons.getBuildType();
         return new EpgFetcherImpl(
                 context,
+                epgInputWhiteList,
                 channelDataManager,
                 epgReader,
                 performanceMonitor,
                 clock,
-                routineIntervalMs);
+                backendKnobsFlags,
+                buildType);
     }
 
     @VisibleForTesting
     EpgFetcherImpl(
             Context context,
+            EpgInputWhiteList epgInputWhiteList,
             ChannelDataManager channelDataManager,
             EpgReader epgReader,
             PerformanceMonitor performanceMonitor,
             Clock clock,
-            long routineIntervalMs) {
+            BackendKnobsFlags backendKnobsFlags,
+            HasBuildType.BuildType buildType) {
         mContext = context;
         mChannelDataManager = channelDataManager;
         mEpgReader = epgReader;
         mPerformanceMonitor = performanceMonitor;
         mClock = clock;
-        mRoutineIntervalMs =
-                routineIntervalMs <= 0
-                        ? TimeUnit.HOURS.toMillis(ROUTINE_INTERVAL_HOUR.getDefaultValue())
-                        : TimeUnit.HOURS.toMillis(routineIntervalMs);
-        mEpgDataExpiredTimeLimitMs = routineIntervalMs * 2;
-        mFastFetchDurationSec = FAST_FETCH_DURATION_SEC + routineIntervalMs / 1000;
+        mEpgInputWhiteList = epgInputWhiteList;
+        mBackendKnobsFlags = backendKnobsFlags;
+        mBuildType = buildType;
+    }
+
+    private long getFastFetchDurationSec() {
+        return FAST_FETCH_DURATION_SEC + getRoutineIntervalMs() / 1000;
+    }
+
+    private long getEpgDataExpiredTimeLimitMs() {
+        return getRoutineIntervalMs() * 2;
+    }
+
+    private long getRoutineIntervalMs() {
+        long routineIntervalHours = mBackendKnobsFlags.epgFetcherIntervalHour();
+        return routineIntervalHours <= 0
+                ? TimeUnit.HOURS.toMillis(DEFAULT_ROUTINE_INTERVAL_HOUR)
+                : TimeUnit.HOURS.toMillis(routineIntervalHours);
     }
 
     private static Set<Channel> getExistingChannelsForMyPackage(Context context) {
@@ -214,7 +233,7 @@
                 new JobInfo.Builder(
                                 EPG_ROUTINELY_FETCHING_JOB_ID,
                                 new ComponentName(mContext, EpgFetchService.class))
-                        .setPeriodic(mRoutineIntervalMs)
+                        .setPeriodic(getRoutineIntervalMs())
                         .setBackoffCriteria(INITIAL_BACKOFF_MS, JobInfo.BACKOFF_POLICY_EXPONENTIAL)
                         .setPersisted(true)
                         .build();
@@ -238,7 +257,7 @@
             @Override
             protected void onPostExecute(Long result) {
                 if (mClock.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)
-                        > mEpgDataExpiredTimeLimitMs) {
+                        > getEpgDataExpiredTimeLimitMs()) {
                     Log.i(TAG, "EPG data expired. Start fetching immediately.");
                     fetchImmediately();
                 }
@@ -346,6 +365,19 @@
             if (DEBUG) Log.d(TAG, "Cannot start routine service: scanning channels.");
             return false;
         }
+        if (!TvFeatures.CLOUD_EPG_FOR_3RD_PARTY.isEnabled(mContext)
+                && mBuildType != HasBuildType.BuildType.AOSP) {
+            if (getTunerChannelCount() == 0) {
+                if (DEBUG) Log.d(TAG, "Cannot start routine service: no internal tuner channels.");
+                return false;
+            }
+            if (!TextUtils.isEmpty(EpgFetchHelper.getLastLineupId(mContext))) {
+                return true;
+            }
+            if (!TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) {
+                return true;
+            }
+        }
         return true;
     }
 
@@ -505,6 +537,17 @@
         return numbers.size();
     }
 
+    private boolean isInputInWhiteList(EpgInput epgInput) {
+        if (mBuildType == HasBuildType.BuildType.AOSP) {
+            return false;
+        }
+        return (BuildConfig.ENG
+                        && epgInput.getInputId()
+                                .equals(
+                                        "com.example.partnersupportsampletvinput/.SampleTvInputService"))
+                || mEpgInputWhiteList.isInputWhiteListed(epgInput.getInputId());
+    }
+
     @VisibleForTesting
     class FetchAsyncTask extends AsyncTask<Void, Void, Integer> {
         private final JobService mService;
@@ -532,12 +575,45 @@
                 Integer builtInResult = fetchEpgForBuiltInTuner();
                 boolean anyCloudEpgFailure = false;
                 boolean anyCloudEpgSuccess = false;
+                if (TvFeatures.CLOUD_EPG_FOR_3RD_PARTY.isEnabled(mContext)
+                        && mBuildType != HasBuildType.BuildType.AOSP) {
+                    for (EpgInput epgInput : getEpgInputs()) {
+                        if (DEBUG) Log.d(TAG, "Start EPG fetch for " + epgInput);
+                        if (isCancelled()) {
+                            break;
+                        }
+                        if (isInputInWhiteList(epgInput)) {
+                            // TODO(b/66191312) check timestamp and result code and decide if update
+                            // is needed.
+                            Set<Channel> channels = getExistingChannelsFor(epgInput.getInputId());
+                            Integer result = fetchEpgFor(epgInput.getLineupId(), channels);
+                            anyCloudEpgFailure = anyCloudEpgFailure || result != null;
+                            anyCloudEpgSuccess = anyCloudEpgSuccess || result == null;
+                            updateCloudEpgInput(epgInput, result);
+                        } else {
+                            Log.w(
+                                    TAG,
+                                    "Fetching the EPG for "
+                                            + epgInput.getInputId()
+                                            + " is not supported.");
+                        }
+                    }
+                }
+                if (builtInResult == null || builtInResult == REASON_NO_BUILT_IN_CHANNELS) {
+                    return anyCloudEpgFailure
+                            ? ((Integer) REASON_CLOUD_EPG_FAILURE)
+                            : anyCloudEpgSuccess ? null : builtInResult;
+                }
                 return builtInResult;
             } finally {
                 TrafficStats.setThreadStatsTag(oldTag);
             }
         }
 
+        private void updateCloudEpgInput(EpgInput unusedEpgInput, Integer unusedResult) {
+            // TODO(b/66191312) write the result and timestamp to the input table
+        }
+
         private Set<Channel> getExistingChannelsFor(String inputId) {
             Set<Channel> result = new HashSet<>();
             try (Cursor cursor =
@@ -548,13 +624,24 @@
                                     null,
                                     null,
                                     null)) {
-                while (cursor.moveToNext()) {
-                    result.add(ChannelImpl.fromCursor(cursor));
+                if (cursor != null) {
+                    while (cursor.moveToNext()) {
+                        result.add(ChannelImpl.fromCursor(cursor));
+                    }
                 }
                 return result;
             }
         }
 
+        private Set<EpgInput> getEpgInputs() {
+            if (mBuildType == HasBuildType.BuildType.AOSP) {
+                return ImmutableSet.of();
+            }
+            Set<EpgInput> epgInputs = EpgInputs.queryEpgInputs(mContext.getContentResolver());
+            if (DEBUG) Log.d(TAG, "getEpgInputs " + epgInputs);
+            return epgInputs;
+        }
+
         private Integer fetchEpgForBuiltInTuner() {
             try {
                 Integer failureReason = prepareFetchEpg(false);
@@ -606,19 +693,16 @@
                 Log.i(TAG, "Failed to get EPG channels for " + lineupId);
                 return REASON_NO_EPG_DATA_RETURNED;
             }
+            EpgFetchHelper.updateNetworkAffiliation(mContext, channels);
             if (mClock.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)
-                    > mEpgDataExpiredTimeLimitMs) {
-                batchFetchEpg(channels, mFastFetchDurationSec);
+                    > getEpgDataExpiredTimeLimitMs()) {
+                batchFetchEpg(channels, getFastFetchDurationSec());
             }
             new Handler(mContext.getMainLooper())
                     .post(
-                            new Runnable() {
-                                @Override
-                                public void run() {
+                            () ->
                                     ChannelLogoFetcher.startFetchingChannelLogos(
-                                            mContext, asChannelList(channels));
-                                }
-                            });
+                                            mContext, asChannelList(channels)));
             for (EpgReader.EpgChannel epgChannel : channels) {
                 if (this.isCancelled()) {
                     return null;
@@ -780,6 +864,9 @@
                     mFetchedChannelIdsDuringScan.add(epgChannel.getChannel().getId());
                 }
             }
+            if (!newChannels.isEmpty()) {
+                EpgFetchHelper.updateNetworkAffiliation(mContext, newChannels);
+            }
             batchFetchEpg(newChannels, FETCH_DURING_SCAN_DURATION_SEC);
         }
 
@@ -798,14 +885,7 @@
             // Clear timestamp to make routine service start right away.
             EpgFetchHelper.setLastEpgUpdatedTimestamp(mContext, 0);
             Log.i(TAG, "EPG Fetching during channel scanning finished.");
-            new Handler(Looper.getMainLooper())
-                    .post(
-                            new Runnable() {
-                                @Override
-                                public void run() {
-                                    fetchImmediately();
-                                }
-                            });
+            new Handler(Looper.getMainLooper()).post(EpgFetcherImpl.this::fetchImmediately);
         }
     }
 }
diff --git a/src/com/android/tv/data/epg/EpgInputWhiteList.java b/src/com/android/tv/data/epg/EpgInputWhiteList.java
index eada8b2..24b4fe3 100644
--- a/src/com/android/tv/data/epg/EpgInputWhiteList.java
+++ b/src/com/android/tv/data/epg/EpgInputWhiteList.java
@@ -21,8 +21,8 @@
 import android.text.TextUtils;
 import android.util.Log;
 import com.android.tv.common.BuildConfig;
-import com.android.tv.common.config.api.RemoteConfig;
 import com.android.tv.common.experiments.Experiments;
+import com.android.tv.common.flags.CloudEpgFlags;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
@@ -33,7 +33,6 @@
 public final class EpgInputWhiteList {
     private static final boolean DEBUG = false;
     private static final String TAG = "EpgInputWhiteList";
-    @VisibleForTesting public static final String KEY = "live_channels_3rd_party_epg_inputs";
     private static final String QA_DEV_INPUTS =
             "com.example.partnersupportsampletvinput/.SampleTvInputService,"
                     + "com.android.tv.tuner.sample.dvb/.tvinput.SampleDvbTunerTvInputService";
@@ -44,10 +43,10 @@
         return inputId == null ? null : inputId.substring(0, inputId.indexOf("/"));
     }
 
-    private final RemoteConfig remoteConfig;
+    private final CloudEpgFlags cloudEpgFlags;
 
-    public EpgInputWhiteList(RemoteConfig remoteConfig) {
-        this.remoteConfig = remoteConfig;
+    public EpgInputWhiteList(CloudEpgFlags cloudEpgFlags) {
+        this.cloudEpgFlags = cloudEpgFlags;
     }
 
     public boolean isInputWhiteListed(String inputId) {
@@ -72,7 +71,7 @@
     }
 
     private Set<String> getWhiteListedInputs() {
-        Set<String> result = toInputSet(remoteConfig.getString(KEY));
+        Set<String> result = toInputSet(cloudEpgFlags.thirdPartyEpgInputsCsv());
         if (BuildConfig.ENG || Experiments.ENABLE_QA_FEATURES.get()) {
             HashSet<String> moreInputs = new HashSet<>(toInputSet(QA_DEV_INPUTS));
             if (result.isEmpty()) {
diff --git a/src/com/android/tv/data/epg/EpgReader.java b/src/com/android/tv/data/epg/EpgReader.java
index 7147905..c9fcd97 100644
--- a/src/com/android/tv/data/epg/EpgReader.java
+++ b/src/com/android/tv/data/epg/EpgReader.java
@@ -23,6 +23,7 @@
 import com.android.tv.data.Program;
 import com.android.tv.data.api.Channel;
 import com.android.tv.dvr.data.SeriesInfo;
+import com.google.auto.value.AutoValue;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -33,15 +34,18 @@
 public interface EpgReader {
 
     /** Value class that holds a EpgChannelId and its corresponding {@link Channel} */
-    // TODO(b/72052568): Get autovalue to work in aosp master
+    @AutoValue
     abstract class EpgChannel {
-        public static EpgChannel createEpgChannel(Channel channel, String epgChannelId) {
-            return new AutoValue_EpgReader_EpgChannel(channel, epgChannelId);
+        public static EpgChannel createEpgChannel(Channel channel, String epgChannelId,
+                boolean dbUpdateNeeded) {
+            return new AutoValue_EpgReader_EpgChannel(channel, epgChannelId, dbUpdateNeeded);
         }
 
         public abstract Channel getChannel();
 
         public abstract String getEpgChannelId();
+
+        public abstract boolean getDbUpdateNeeded();
     }
 
     /** Checks if the reader is available. */
diff --git a/src/com/android/tv/dialog/PinDialogFragment.java b/src/com/android/tv/dialog/PinDialogFragment.java
index 71f45fb..8730809 100644
--- a/src/com/android/tv/dialog/PinDialogFragment.java
+++ b/src/com/android/tv/dialog/PinDialogFragment.java
@@ -16,37 +16,26 @@
 
 package com.android.tv.dialog;
 
-import android.animation.Animator;
-import android.animation.AnimatorInflater;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
 import android.app.ActivityManager;
 import android.app.Dialog;
-import android.content.Context;
 import android.content.DialogInterface;
 import android.content.SharedPreferences;
-import android.content.res.Resources;
 import android.media.tv.TvContentRating;
 import android.os.Bundle;
 import android.os.Handler;
 import android.preference.PreferenceManager;
 import android.text.TextUtils;
-import android.util.AttributeSet;
 import android.util.Log;
-import android.util.TypedValue;
-import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewGroup.LayoutParams;
-import android.widget.FrameLayout;
 import android.widget.TextView;
 import android.widget.Toast;
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
 import com.android.tv.common.SoftPreconditions;
+import com.android.tv.dialog.picker.PinPicker;
 import com.android.tv.util.TvSettings;
 
 public class PinDialogFragment extends SafeDismissDialogFragment {
@@ -77,17 +66,12 @@
     private static final int MAX_WRONG_PIN_COUNT = 5;
     private static final int DISABLE_PIN_DURATION_MILLIS = 60 * 1000; // 1 minute
 
-    private static final String INITIAL_TEXT = "—";
     private static final String TRACKER_LABEL = "Pin dialog";
     private static final String ARGS_TYPE = "args_type";
     private static final String ARGS_RATING = "args_rating";
 
     public static final String DIALOG_TAG = PinDialogFragment.class.getName();
 
-    private static final int NUMBER_PICKERS_RES_ID[] = {
-        R.id.first, R.id.second, R.id.third, R.id.fourth
-    };
-
     private int mType;
     private int mRequestType;
     private boolean mPinChecked;
@@ -96,7 +80,7 @@
     private TextView mWrongPinView;
     private View mEnterPinView;
     private TextView mTitleView;
-    private PinNumberPicker[] mPickers;
+    private PinPicker mPicker;
     private SharedPreferences mSharedPreferences;
     private String mPrevPin;
     private String mPin;
@@ -140,7 +124,6 @@
     public Dialog onCreateDialog(Bundle savedInstanceState) {
         Dialog dlg = super.onCreateDialog(savedInstanceState);
         dlg.getWindow().getAttributes().windowAnimations = R.style.pin_dialog_animation;
-        PinNumberPicker.loadResources(dlg.getContext());
         return dlg;
     }
 
@@ -171,6 +154,14 @@
         mWrongPinView = (TextView) v.findViewById(R.id.wrong_pin);
         mEnterPinView = v.findViewById(R.id.enter_pin);
         mTitleView = (TextView) mEnterPinView.findViewById(R.id.title);
+        mPicker = v.findViewById(R.id.pin_picker);
+        mPicker.setOnClickListener(
+                view -> {
+                    String pin = getPinInput();
+                    if (!TextUtils.isEmpty(pin)) {
+                        done(pin);
+                    }
+                });
         if (TextUtils.isEmpty(getPin())) {
             // If PIN isn't set, user should set a PIN.
             // Successfully setting a new set is considered as entering correct PIN.
@@ -210,31 +201,13 @@
                 }
         }
 
-        mPickers = new PinNumberPicker[NUMBER_PICKERS_RES_ID.length];
-        for (int i = 0; i < NUMBER_PICKERS_RES_ID.length; i++) {
-            mPickers[i] = (PinNumberPicker) v.findViewById(NUMBER_PICKERS_RES_ID[i]);
-            mPickers[i].setValueRangeAndResetText(0, 9);
-            mPickers[i].setPinDialogFragment(this);
-            mPickers[i].updateFocus(false);
-        }
-        for (int i = 0; i < NUMBER_PICKERS_RES_ID.length - 1; i++) {
-            mPickers[i].setNextNumberPicker(mPickers[i + 1]);
-        }
-
         if (mType != PIN_DIALOG_TYPE_NEW_PIN) {
             updateWrongPin();
         }
+        mPicker.requestFocus();
         return v;
     }
 
-    private final Runnable mUpdateEnterPinRunnable =
-            new Runnable() {
-                @Override
-                public void run() {
-                    updateWrongPin();
-                }
-            };
-
     private void updateWrongPin() {
         if (getActivity() == null) {
             // The activity is already detached. No need to update.
@@ -257,7 +230,8 @@
                                     R.plurals.pin_enter_countdown,
                                     remainingSeconds,
                                     remainingSeconds));
-            mHandler.postDelayed(mUpdateEnterPinRunnable, 1000);
+
+            mHandler.postDelayed(this::updateWrongPin, 1000);
         }
     }
 
@@ -364,383 +338,11 @@
     }
 
     private String getPinInput() {
-        String result = "";
-        try {
-            for (PinNumberPicker pnp : mPickers) {
-                pnp.updateText();
-                result += pnp.getValue();
-            }
-        } catch (IllegalStateException e) {
-            result = "";
-        }
-        return result;
+        return mPicker.getPinInput();
     }
 
     private void resetPinInput() {
-        for (PinNumberPicker pnp : mPickers) {
-            pnp.setValueRangeAndResetText(0, 9);
-        }
-        mPickers[0].requestFocus();
-    }
-
-    public static class PinNumberPicker extends FrameLayout {
-        private static final int NUMBER_VIEWS_RES_ID[] = {
-            R.id.previous2_number,
-            R.id.previous_number,
-            R.id.current_number,
-            R.id.next_number,
-            R.id.next2_number
-        };
-        private static final int CURRENT_NUMBER_VIEW_INDEX = 2;
-        private static final int NOT_INITIALIZED = Integer.MIN_VALUE;
-
-        private static Animator sFocusedNumberEnterAnimator;
-        private static Animator sFocusedNumberExitAnimator;
-        private static Animator sAdjacentNumberEnterAnimator;
-        private static Animator sAdjacentNumberExitAnimator;
-
-        private static float sAlphaForFocusedNumber;
-        private static float sAlphaForAdjacentNumber;
-
-        private int mMinValue;
-        private int mMaxValue;
-        private int mCurrentValue;
-        // a value for setting mCurrentValue at the end of scroll animation.
-        private int mNextValue;
-        private final int mNumberViewHeight;
-        private PinDialogFragment mDialog;
-        private PinNumberPicker mNextNumberPicker;
-        private boolean mCancelAnimation;
-
-        private final View mNumberViewHolder;
-        // When the PinNumberPicker has focus, mBackgroundView will show the focused background.
-        // Also, this view is used for handling the text change animation of the current number
-        // view which is required when the current number view text is changing from INITIAL_TEXT
-        // to "0".
-        private final TextView mBackgroundView;
-        private final TextView[] mNumberViews;
-        private final AnimatorSet mFocusGainAnimator;
-        private final AnimatorSet mFocusLossAnimator;
-        private final AnimatorSet mScrollAnimatorSet;
-
-        public PinNumberPicker(Context context) {
-            this(context, null);
-        }
-
-        public PinNumberPicker(Context context, AttributeSet attrs) {
-            this(context, attrs, 0);
-        }
-
-        public PinNumberPicker(Context context, AttributeSet attrs, int defStyleAttr) {
-            this(context, attrs, defStyleAttr, 0);
-        }
-
-        public PinNumberPicker(
-                Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
-            super(context, attrs, defStyleAttr, defStyleRes);
-            View view = inflate(context, R.layout.pin_number_picker, this);
-            mNumberViewHolder = view.findViewById(R.id.number_view_holder);
-            mBackgroundView = (TextView) view.findViewById(R.id.focused_background);
-            mNumberViews = new TextView[NUMBER_VIEWS_RES_ID.length];
-            for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) {
-                mNumberViews[i] = (TextView) view.findViewById(NUMBER_VIEWS_RES_ID[i]);
-            }
-            Resources resources = context.getResources();
-            mNumberViewHeight =
-                    resources.getDimensionPixelSize(R.dimen.pin_number_picker_text_view_height);
-
-            mNumberViewHolder.setOnFocusChangeListener(
-                    new OnFocusChangeListener() {
-                        @Override
-                        public void onFocusChange(View v, boolean hasFocus) {
-                            updateFocus(true);
-                        }
-                    });
-
-            mNumberViewHolder.setOnKeyListener(
-                    new OnKeyListener() {
-                        @Override
-                        public boolean onKey(View v, int keyCode, KeyEvent event) {
-                            if (event.getAction() == KeyEvent.ACTION_DOWN) {
-                                switch (keyCode) {
-                                    case KeyEvent.KEYCODE_DPAD_UP:
-                                    case KeyEvent.KEYCODE_DPAD_DOWN:
-                                        {
-                                            if (mCancelAnimation) {
-                                                mScrollAnimatorSet.end();
-                                            }
-                                            if (!mScrollAnimatorSet.isRunning()) {
-                                                mCancelAnimation = false;
-                                                if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
-                                                    mNextValue =
-                                                            adjustValueInValidRange(
-                                                                    mCurrentValue + 1);
-                                                    startScrollAnimation(true);
-                                                } else {
-                                                    mNextValue =
-                                                            adjustValueInValidRange(
-                                                                    mCurrentValue - 1);
-                                                    startScrollAnimation(false);
-                                                }
-                                            }
-                                            return true;
-                                        }
-                                }
-                            } else if (event.getAction() == KeyEvent.ACTION_UP) {
-                                switch (keyCode) {
-                                    case KeyEvent.KEYCODE_DPAD_UP:
-                                    case KeyEvent.KEYCODE_DPAD_DOWN:
-                                        {
-                                            mCancelAnimation = true;
-                                            return true;
-                                        }
-                                }
-                            }
-                            return false;
-                        }
-                    });
-            mNumberViewHolder.setScrollY(mNumberViewHeight);
-
-            mFocusGainAnimator = new AnimatorSet();
-            mFocusGainAnimator.playTogether(
-                    ObjectAnimator.ofFloat(
-                            mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1],
-                            "alpha",
-                            0f,
-                            sAlphaForAdjacentNumber),
-                    ObjectAnimator.ofFloat(
-                            mNumberViews[CURRENT_NUMBER_VIEW_INDEX],
-                            "alpha",
-                            sAlphaForFocusedNumber,
-                            0f),
-                    ObjectAnimator.ofFloat(
-                            mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1],
-                            "alpha",
-                            0f,
-                            sAlphaForAdjacentNumber),
-                    ObjectAnimator.ofFloat(mBackgroundView, "alpha", 0f, 1f));
-            mFocusGainAnimator.setDuration(
-                    context.getResources().getInteger(android.R.integer.config_shortAnimTime));
-            mFocusGainAnimator.addListener(
-                    new AnimatorListenerAdapter() {
-                        @Override
-                        public void onAnimationEnd(Animator animator) {
-                            mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setText(
-                                    mBackgroundView.getText());
-                            mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setAlpha(
-                                    sAlphaForFocusedNumber);
-                            mBackgroundView.setText("");
-                        }
-                    });
-
-            mFocusLossAnimator = new AnimatorSet();
-            mFocusLossAnimator.playTogether(
-                    ObjectAnimator.ofFloat(
-                            mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1],
-                            "alpha",
-                            sAlphaForAdjacentNumber,
-                            0f),
-                    ObjectAnimator.ofFloat(
-                            mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1],
-                            "alpha",
-                            sAlphaForAdjacentNumber,
-                            0f),
-                    ObjectAnimator.ofFloat(mBackgroundView, "alpha", 1f, 0f));
-            mFocusLossAnimator.setDuration(
-                    context.getResources().getInteger(android.R.integer.config_shortAnimTime));
-
-            mScrollAnimatorSet = new AnimatorSet();
-            mScrollAnimatorSet.setDuration(
-                    context.getResources().getInteger(R.integer.pin_number_scroll_duration));
-            mScrollAnimatorSet.addListener(
-                    new AnimatorListenerAdapter() {
-                        @Override
-                        public void onAnimationEnd(Animator animation) {
-                            // Set mCurrent value when scroll animation is finished.
-                            mCurrentValue = mNextValue;
-                            updateText();
-                            mNumberViewHolder.setScrollY(mNumberViewHeight);
-                            mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1].setAlpha(
-                                    sAlphaForAdjacentNumber);
-                            mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setAlpha(
-                                    sAlphaForFocusedNumber);
-                            mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1].setAlpha(
-                                    sAlphaForAdjacentNumber);
-                        }
-                    });
-        }
-
-        static void loadResources(Context context) {
-            if (sFocusedNumberEnterAnimator == null) {
-                TypedValue outValue = new TypedValue();
-                context.getResources()
-                        .getValue(R.dimen.pin_alpha_for_focused_number, outValue, true);
-                sAlphaForFocusedNumber = outValue.getFloat();
-                context.getResources()
-                        .getValue(R.dimen.pin_alpha_for_adjacent_number, outValue, true);
-                sAlphaForAdjacentNumber = outValue.getFloat();
-
-                sFocusedNumberEnterAnimator =
-                        AnimatorInflater.loadAnimator(context, R.animator.pin_focused_number_enter);
-                sFocusedNumberExitAnimator =
-                        AnimatorInflater.loadAnimator(context, R.animator.pin_focused_number_exit);
-                sAdjacentNumberEnterAnimator =
-                        AnimatorInflater.loadAnimator(
-                                context, R.animator.pin_adjacent_number_enter);
-                sAdjacentNumberExitAnimator =
-                        AnimatorInflater.loadAnimator(context, R.animator.pin_adjacent_number_exit);
-            }
-        }
-
-        @Override
-        public boolean dispatchKeyEvent(KeyEvent event) {
-            if (event.getAction() == KeyEvent.ACTION_UP) {
-                int keyCode = event.getKeyCode();
-                if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
-                    mNextValue = adjustValueInValidRange(keyCode - KeyEvent.KEYCODE_0);
-                    updateFocus(false);
-                } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER
-                        || keyCode == KeyEvent.KEYCODE_ENTER) {
-                    if (mNextNumberPicker == null) {
-                        String pin = mDialog.getPinInput();
-                        if (!TextUtils.isEmpty(pin)) {
-                            mDialog.done(pin);
-                        }
-                    } else {
-                        mNextNumberPicker.requestFocus();
-                    }
-                    return true;
-                }
-            }
-            return super.dispatchKeyEvent(event);
-        }
-
-        void startScrollAnimation(boolean scrollUp) {
-            mFocusGainAnimator.end();
-            mFocusLossAnimator.end();
-            final ValueAnimator scrollAnimator =
-                    ValueAnimator.ofInt(0, scrollUp ? mNumberViewHeight : -mNumberViewHeight);
-            scrollAnimator.addUpdateListener(
-                    new ValueAnimator.AnimatorUpdateListener() {
-                        @Override
-                        public void onAnimationUpdate(ValueAnimator animation) {
-                            int value = (Integer) animation.getAnimatedValue();
-                            mNumberViewHolder.setScrollY(value + mNumberViewHeight);
-                        }
-                    });
-            scrollAnimator.setDuration(
-                    getResources().getInteger(R.integer.pin_number_scroll_duration));
-
-            if (scrollUp) {
-                sAdjacentNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1]);
-                sFocusedNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX]);
-                sFocusedNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1]);
-                sAdjacentNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 2]);
-            } else {
-                sAdjacentNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 2]);
-                sFocusedNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1]);
-                sFocusedNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX]);
-                sAdjacentNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1]);
-            }
-
-            mScrollAnimatorSet.playTogether(
-                    scrollAnimator,
-                    sAdjacentNumberExitAnimator,
-                    sFocusedNumberExitAnimator,
-                    sFocusedNumberEnterAnimator,
-                    sAdjacentNumberEnterAnimator);
-            mScrollAnimatorSet.start();
-        }
-
-        void setValueRangeAndResetText(int min, int max) {
-            if (min > max) {
-                throw new IllegalArgumentException(
-                        "The min value should be greater than or equal to the max value");
-            } else if (min == NOT_INITIALIZED) {
-                throw new IllegalArgumentException(
-                        "The min value should be greater than Integer.MIN_VALUE.");
-            }
-            mMinValue = min;
-            mMaxValue = max;
-            mNextValue = mCurrentValue = NOT_INITIALIZED;
-            for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) {
-                mNumberViews[i].setText(i == CURRENT_NUMBER_VIEW_INDEX ? INITIAL_TEXT : "");
-            }
-            mBackgroundView.setText(INITIAL_TEXT);
-        }
-
-        void setPinDialogFragment(PinDialogFragment dlg) {
-            mDialog = dlg;
-        }
-
-        void setNextNumberPicker(PinNumberPicker picker) {
-            mNextNumberPicker = picker;
-        }
-
-        int getValue() {
-            if (mCurrentValue < mMinValue || mCurrentValue > mMaxValue) {
-                throw new IllegalStateException("Value is not set");
-            }
-            return mCurrentValue;
-        }
-
-        void updateFocus(boolean withAnimation) {
-            mScrollAnimatorSet.end();
-            mFocusGainAnimator.end();
-            mFocusLossAnimator.end();
-            updateText();
-            if (mNumberViewHolder.isFocused()) {
-                if (withAnimation) {
-                    mBackgroundView.setText(String.valueOf(mCurrentValue));
-                    mFocusGainAnimator.start();
-                } else {
-                    mBackgroundView.setAlpha(1f);
-                    mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1].setAlpha(sAlphaForAdjacentNumber);
-                    mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1].setAlpha(sAlphaForAdjacentNumber);
-                }
-            } else {
-                if (withAnimation) {
-                    mFocusLossAnimator.start();
-                } else {
-                    mBackgroundView.setAlpha(0f);
-                    mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1].setAlpha(0f);
-                    mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1].setAlpha(0f);
-                }
-                mNumberViewHolder.setScrollY(mNumberViewHeight);
-            }
-        }
-
-        private void updateText() {
-            boolean wasNotInitialized = false;
-            if (mNumberViewHolder.isFocused() && mCurrentValue == NOT_INITIALIZED) {
-                mNextValue = mCurrentValue = mMinValue;
-                wasNotInitialized = true;
-            }
-            if (mCurrentValue >= mMinValue && mCurrentValue <= mMaxValue) {
-                for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) {
-                    if (wasNotInitialized && i == CURRENT_NUMBER_VIEW_INDEX) {
-                        // In order to show the text change animation, keep the text of
-                        // mNumberViews[CURRENT_NUMBER_VIEW_INDEX].
-                    } else {
-                        mNumberViews[i].setText(
-                                String.valueOf(
-                                        adjustValueInValidRange(
-                                                mCurrentValue - CURRENT_NUMBER_VIEW_INDEX + i)));
-                    }
-                }
-            }
-        }
-
-        private int adjustValueInValidRange(int value) {
-            int interval = mMaxValue - mMinValue + 1;
-            if (value < mMinValue - interval || value > mMaxValue + interval) {
-                throw new IllegalArgumentException(
-                        "The value( " + value + ") is too small or too big to adjust");
-            }
-            return (value < mMinValue)
-                    ? value + interval
-                    : (value > mMaxValue) ? value - interval : value;
-        }
+        mPicker.resetPinInput();
     }
 
     /**
diff --git a/src/com/android/tv/dialog/picker/PinPicker.java b/src/com/android/tv/dialog/picker/PinPicker.java
new file mode 100644
index 0000000..f501dfd
--- /dev/null
+++ b/src/com/android/tv/dialog/picker/PinPicker.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.dialog.picker;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v17.leanback.widget.picker.Picker;
+import android.support.v17.leanback.widget.picker.PickerColumn;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import java.util.ArrayList;
+import java.util.List;
+
+/** 4 digit picker */
+public final class PinPicker extends Picker {
+    // TODO(b/116144491): use leanback pin picker.
+
+    private final List<PickerColumn> mPickers = new ArrayList<>();
+    private OnClickListener mOnClickListener;
+
+    // the version of picker I link to does not have this constructor
+    public PinPicker(Context context, AttributeSet attributeSet) {
+        this(context, attributeSet, 0);
+    }
+
+    public PinPicker(Context context, AttributeSet attributeSet, int defStyleAttr) {
+        super(context, attributeSet, defStyleAttr);
+
+        for (int i = 0; i < 4; i++) {
+            PickerColumn pickerColumn = new PickerColumn();
+            pickerColumn.setMinValue(0);
+            pickerColumn.setMaxValue(9);
+            pickerColumn.setLabelFormat("%d");
+            mPickers.add(pickerColumn);
+        }
+        setSeparator(" ");
+        setColumns(mPickers);
+        setActivated(true);
+        setFocusable(true);
+        super.setOnClickListener(this::onClick);
+    }
+
+    public String getPinInput() {
+        String result = "";
+        try {
+            for (PickerColumn column : mPickers) {
+
+                result += column.getCurrentValue();
+            }
+        } catch (IllegalStateException e) {
+            result = "";
+        }
+        return result;
+    }
+
+    @Override
+    public void setOnClickListener(@Nullable OnClickListener l) {
+        mOnClickListener = l;
+    }
+
+    private void onClick(View v) {
+        int selectedColumn = getSelectedColumn();
+        int nextColumn = selectedColumn + 1;
+        // Only call the click listener if we are on the last column
+        // Otherwise move to the next column
+        if (nextColumn == getColumnsCount()) {
+            if (mOnClickListener != null) {
+                mOnClickListener.onClick(v);
+            }
+        } else {
+            setSelectedColumn(nextColumn);
+            onRequestFocusInDescendants(ViewGroup.FOCUS_FORWARD, null);
+        }
+    }
+
+    public void resetPinInput() {
+        setActivated(false);
+        for (int i = 0; i < 4; i++) {
+            setColumnValue(i, 0, true);
+        }
+        setSelectedColumn(0);
+        setActivated(true); // This resets the focus
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        if (event.getAction() == KeyEvent.ACTION_UP) {
+            int keyCode = event.getKeyCode();
+            int digit = digitFromKeyCode(keyCode);
+            if (digit != -1) {
+                int selectedColumn = getSelectedColumn();
+                setColumnValue(selectedColumn, digit, false);
+                int nextColumn = selectedColumn + 1;
+                if (nextColumn < getColumnsCount()) {
+                    setSelectedColumn(nextColumn);
+                    onRequestFocusInDescendants(ViewGroup.FOCUS_FORWARD, null);
+                } else {
+                    callOnClick();
+                }
+                return true;
+            }
+        }
+        return super.dispatchKeyEvent(event);
+    }
+
+    @VisibleForTesting
+    static int digitFromKeyCode(int keyCode) {
+        if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
+            return keyCode - KeyEvent.KEYCODE_0;
+        } else if (keyCode >= KeyEvent.KEYCODE_NUMPAD_0 && keyCode <= KeyEvent.KEYCODE_NUMPAD_9) {
+            return keyCode - KeyEvent.KEYCODE_NUMPAD_0;
+        }
+        return -1;
+    }
+}
diff --git a/src/com/android/tv/dvr/DvrDataManagerImpl.java b/src/com/android/tv/dvr/DvrDataManagerImpl.java
index 2b4ecbf..0053650 100644
--- a/src/com/android/tv/dvr/DvrDataManagerImpl.java
+++ b/src/com/android/tv/dvr/DvrDataManagerImpl.java
@@ -16,7 +16,6 @@
 
 package com.android.tv.dvr;
 
-import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
 import android.content.ContentResolver;
 import android.content.ContentUris;
@@ -49,21 +48,23 @@
 import com.android.tv.dvr.data.ScheduledRecording;
 import com.android.tv.dvr.data.ScheduledRecording.RecordingState;
 import com.android.tv.dvr.data.SeriesRecording;
-import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddScheduleTask;
-import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddSeriesRecordingTask;
-import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDeleteScheduleTask;
-import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDeleteSeriesRecordingTask;
-import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQueryScheduleTask;
-import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQuerySeriesRecordingTask;
-import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncUpdateScheduleTask;
-import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncUpdateSeriesRecordingTask;
+import com.android.tv.dvr.provider.DvrDbFuture.AddScheduleFuture;
+import com.android.tv.dvr.provider.DvrDbFuture.AddSeriesRecordingFuture;
+import com.android.tv.dvr.provider.DvrDbFuture.DeleteScheduleFuture;
+import com.android.tv.dvr.provider.DvrDbFuture.DeleteSeriesRecordingFuture;
+import com.android.tv.dvr.provider.DvrDbFuture.DvrQueryScheduleFuture;
+import com.android.tv.dvr.provider.DvrDbFuture.DvrQuerySeriesRecordingFuture;
+import com.android.tv.dvr.provider.DvrDbFuture.UpdateScheduleFuture;
+import com.android.tv.dvr.provider.DvrDbFuture.UpdateSeriesRecordingFuture;
 import com.android.tv.dvr.provider.DvrDbSync;
 import com.android.tv.dvr.recorder.SeriesRecordingScheduler;
 import com.android.tv.util.AsyncDbTask;
 import com.android.tv.util.AsyncDbTask.AsyncRecordedProgramQueryTask;
-import com.android.tv.util.Filter;
 import com.android.tv.util.TvInputManagerHelper;
 import com.android.tv.util.TvUriMatcher;
+import com.google.common.base.Predicate;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.ListenableFuture;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -73,6 +74,7 @@
 import java.util.Map.Entry;
 import java.util.Set;
 import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
 
 /** DVR Data manager to handle recordings and schedules. */
 @MainThread
@@ -106,8 +108,7 @@
 
                 @Override
                 public void onChange(boolean selfChange, final @Nullable Uri uri) {
-                    RecordedProgramsQueryTask task =
-                            new RecordedProgramsQueryTask(mContext.getContentResolver(), uri);
+                    RecordedProgramsQueryTask task = new RecordedProgramsQueryTask(uri);
                     task.executeOnDbThread();
                     mPendingTasks.add(task);
                 }
@@ -116,6 +117,9 @@
     private boolean mDvrLoadFinished;
     private boolean mRecordedProgramLoadFinished;
     private final Set<AsyncTask> mPendingTasks = new ArraySet<>();
+    private final Set<Future> mPendingDvrFuture = new ArraySet<>();
+    // TODO(b/79207567) make sure Future is not stopped at writing.
+    private final Set<Future> mNoStopFuture = new ArraySet<>();
     private DvrDbSync mDbSync;
     private RecordingStorageStatusManager mStorageStatusManager;
 
@@ -154,13 +158,27 @@
                 }
             };
 
+    private final FutureCallback<Void> removeFromSetOnCompletion =
+            new FutureCallback<Void>() {
+                @Override
+                public void onSuccess(Void result) {
+                    mNoStopFuture.remove(this);
+                }
+
+                @Override
+                public void onFailure(Throwable t) {
+                    Log.w(TAG, "Failed to execute.", t);
+                    mNoStopFuture.remove(this);
+                }
+            };
+
     private static <T> List<T> moveElements(
-            HashMap<Long, T> from, HashMap<Long, T> to, Filter<T> filter) {
+            HashMap<Long, T> from, HashMap<Long, T> to, Predicate<T> filter) {
         List<T> moved = new ArrayList<>();
         Iterator<Entry<Long, T>> iter = from.entrySet().iterator();
         while (iter.hasNext()) {
             Entry<Long, T> entry = iter.next();
-            if (filter.filter(entry.getValue())) {
+            if (filter.apply(entry.getValue())) {
                 to.put(entry.getKey(), entry.getValue());
                 iter.remove();
                 moved.add(entry.getValue());
@@ -181,134 +199,143 @@
     public void start() {
         mInputManager.addCallback(mInputCallback);
         mStorageStatusManager.addListener(mStorageMountChangedListener);
-        AsyncDvrQuerySeriesRecordingTask dvrQuerySeriesRecordingTask =
-                new AsyncDvrQuerySeriesRecordingTask(mContext) {
-                    @Override
-                    protected void onCancelled(List<SeriesRecording> seriesRecordings) {
-                        mPendingTasks.remove(this);
-                    }
-
-                    @Override
-                    protected void onPostExecute(List<SeriesRecording> seriesRecordings) {
-                        mPendingTasks.remove(this);
-                        long maxId = 0;
-                        HashSet<String> seriesIds = new HashSet<>();
-                        for (SeriesRecording r : seriesRecordings) {
-                            if (SoftPreconditions.checkState(
-                                    !seriesIds.contains(r.getSeriesId()),
-                                    TAG,
-                                    "Skip loading series recording with duplicate series ID: "
-                                            + r)) {
-                                seriesIds.add(r.getSeriesId());
-                                if (isInputAvailable(r.getInputId())) {
-                                    mSeriesRecordings.put(r.getId(), r);
-                                    mSeriesId2SeriesRecordings.put(r.getSeriesId(), r);
-                                } else {
-                                    mSeriesRecordingsForRemovedInput.put(r.getId(), r);
-                                }
-                            }
-                            if (maxId < r.getId()) {
-                                maxId = r.getId();
-                            }
-                        }
-                        IdGenerator.SERIES_RECORDING.setMaxId(maxId);
-                    }
-                };
-        dvrQuerySeriesRecordingTask.executeOnDbThread();
-        mPendingTasks.add(dvrQuerySeriesRecordingTask);
-        AsyncDvrQueryScheduleTask dvrQueryScheduleTask =
-                new AsyncDvrQueryScheduleTask(mContext) {
-                    @Override
-                    protected void onCancelled(List<ScheduledRecording> scheduledRecordings) {
-                        mPendingTasks.remove(this);
-                    }
-
-                    @SuppressLint("SwitchIntDef")
-                    @Override
-                    protected void onPostExecute(List<ScheduledRecording> result) {
-                        mPendingTasks.remove(this);
-                        long maxId = 0;
-                        int reasonNotStarted =
-                                ScheduledRecording
-                                        .FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED;
-                        List<ScheduledRecording> toUpdate = new ArrayList<>();
-                        List<ScheduledRecording> toDelete = new ArrayList<>();
-                        for (ScheduledRecording r : result) {
-                            if (!isInputAvailable(r.getInputId())) {
-                                mScheduledRecordingsForRemovedInput.put(r.getId(), r);
-                            } else if (r.getState() == ScheduledRecording.STATE_RECORDING_DELETED) {
-                                getDeletedScheduleMap().put(r.getProgramId(), r);
-                            } else {
-                                mScheduledRecordings.put(r.getId(), r);
-                                if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) {
-                                    mProgramId2ScheduledRecordings.put(r.getProgramId(), r);
-                                }
-                                // Adjust the state of the schedules before DB loading is finished.
-                                switch (r.getState()) {
-                                    case ScheduledRecording.STATE_RECORDING_IN_PROGRESS:
-                                        if (r.getEndTimeMs() <= mClock.currentTimeMillis()) {
-                                            int reason =
-                                                    ScheduledRecording.FAILED_REASON_NOT_FINISHED;
-                                            toUpdate.add(
-                                                    ScheduledRecording.buildFrom(r)
-                                                            .setState(
-                                                                    ScheduledRecording
-                                                                            .STATE_RECORDING_FAILED)
-                                                            .setFailedReason(reason)
-                                                            .build());
+        DvrQuerySeriesRecordingFuture dvrQuerySeriesRecordingTask =
+                new DvrQuerySeriesRecordingFuture(mContext);
+        ListenableFuture<List<SeriesRecording>> dvrQuerySeriesRecordingFuture =
+                dvrQuerySeriesRecordingTask.executeOnDbThread(
+                        new FutureCallback<List<SeriesRecording>>() {
+                            @Override
+                            public void onSuccess(List<SeriesRecording> seriesRecordings) {
+                                mPendingDvrFuture.remove(this);
+                                long maxId = 0;
+                                HashSet<String> seriesIds = new HashSet<>();
+                                for (SeriesRecording r : seriesRecordings) {
+                                    if (SoftPreconditions.checkState(
+                                            !seriesIds.contains(r.getSeriesId()),
+                                            TAG,
+                                            "Skip loading series recording with duplicate series ID: "
+                                                    + r)) {
+                                        seriesIds.add(r.getSeriesId());
+                                        if (isInputAvailable(r.getInputId())) {
+                                            mSeriesRecordings.put(r.getId(), r);
+                                            mSeriesId2SeriesRecordings.put(r.getSeriesId(), r);
                                         } else {
-                                            toUpdate.add(
-                                                    ScheduledRecording.buildFrom(r)
-                                                            .setState(
-                                                                    ScheduledRecording
-                                                                            .STATE_RECORDING_NOT_STARTED)
-                                                            .build());
+                                            mSeriesRecordingsForRemovedInput.put(r.getId(), r);
                                         }
-                                        break;
-                                    case ScheduledRecording.STATE_RECORDING_NOT_STARTED:
-                                        if (r.getEndTimeMs() <= mClock.currentTimeMillis()) {
-                                            toUpdate.add(
-                                                    ScheduledRecording.buildFrom(r)
-                                                            .setState(
-                                                                    ScheduledRecording
-                                                                            .STATE_RECORDING_FAILED)
-                                                            .setFailedReason(reasonNotStarted)
-                                                            .build());
+                                    }
+                                    if (maxId < r.getId()) {
+                                        maxId = r.getId();
+                                    }
+                                }
+                                IdGenerator.SERIES_RECORDING.setMaxId(maxId);
+                            }
+
+                            @Override
+                            public void onFailure(Throwable t) {
+                                Log.w(TAG, "Failed to load series recording.", t);
+                                mPendingDvrFuture.remove(this);
+                            }
+                        });
+        mPendingDvrFuture.add(dvrQuerySeriesRecordingFuture);
+        DvrQueryScheduleFuture dvrQueryScheduleTask = new DvrQueryScheduleFuture(mContext);
+        ListenableFuture<List<ScheduledRecording>> dvrQueryScheduleFuture =
+                dvrQueryScheduleTask.executeOnDbThread(
+                        new FutureCallback<List<ScheduledRecording>>() {
+                            @Override
+                            public void onSuccess(List<ScheduledRecording> result) {
+                                mPendingDvrFuture.remove(this);
+                                long maxId = 0;
+                                int reasonNotStarted =
+                                        ScheduledRecording
+                                                .FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED;
+                                List<ScheduledRecording> toUpdate = new ArrayList<>();
+                                List<ScheduledRecording> toDelete = new ArrayList<>();
+                                for (ScheduledRecording r : result) {
+                                    if (!isInputAvailable(r.getInputId())) {
+                                        mScheduledRecordingsForRemovedInput.put(r.getId(), r);
+                                    } else if (r.getState()
+                                            == ScheduledRecording.STATE_RECORDING_DELETED) {
+                                        getDeletedScheduleMap().put(r.getProgramId(), r);
+                                    } else {
+                                        mScheduledRecordings.put(r.getId(), r);
+                                        if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) {
+                                            mProgramId2ScheduledRecordings.put(r.getProgramId(), r);
                                         }
-                                        break;
-                                    case ScheduledRecording.STATE_RECORDING_CANCELED:
-                                        toDelete.add(r);
-                                        break;
-                                    default: // fall out
+                                        // Adjust the state of the schedules before DB loading is
+                                        // finished.
+                                        switch (r.getState()) {
+                                            case ScheduledRecording.STATE_RECORDING_IN_PROGRESS:
+                                                if (r.getEndTimeMs()
+                                                        <= mClock.currentTimeMillis()) {
+                                                    int reason =
+                                                            ScheduledRecording
+                                                                    .FAILED_REASON_NOT_FINISHED;
+                                                    toUpdate.add(
+                                                            ScheduledRecording.buildFrom(r)
+                                                                    .setState(
+                                                                            ScheduledRecording
+                                                                                    .STATE_RECORDING_FAILED)
+                                                                    .setFailedReason(reason)
+                                                                    .build());
+                                                } else {
+                                                    toUpdate.add(
+                                                            ScheduledRecording.buildFrom(r)
+                                                                    .setState(
+                                                                            ScheduledRecording
+                                                                                    .STATE_RECORDING_NOT_STARTED)
+                                                                    .build());
+                                                }
+                                                break;
+                                            case ScheduledRecording.STATE_RECORDING_NOT_STARTED:
+                                                if (r.getEndTimeMs()
+                                                        <= mClock.currentTimeMillis()) {
+                                                    toUpdate.add(
+                                                            ScheduledRecording.buildFrom(r)
+                                                                    .setState(
+                                                                            ScheduledRecording
+                                                                                    .STATE_RECORDING_FAILED)
+                                                                    .setFailedReason(
+                                                                            reasonNotStarted)
+                                                                    .build());
+                                                }
+                                                break;
+                                            case ScheduledRecording.STATE_RECORDING_CANCELED:
+                                                toDelete.add(r);
+                                                break;
+                                            default: // fall out
+                                        }
+                                    }
+                                    if (maxId < r.getId()) {
+                                        maxId = r.getId();
+                                    }
+                                }
+                                if (!toUpdate.isEmpty()) {
+                                    updateScheduledRecording(ScheduledRecording.toArray(toUpdate));
+                                }
+                                if (!toDelete.isEmpty()) {
+                                    removeScheduledRecording(ScheduledRecording.toArray(toDelete));
+                                }
+                                IdGenerator.SCHEDULED_RECORDING.setMaxId(maxId);
+                                if (mRecordedProgramLoadFinished) {
+                                    validateSeriesRecordings();
+                                }
+                                mDvrLoadFinished = true;
+                                notifyDvrScheduleLoadFinished();
+                                if (isInitialized()) {
+                                    mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this);
+                                    mDbSync.start();
+                                    SeriesRecordingScheduler.getInstance(mContext).start();
                                 }
                             }
-                            if (maxId < r.getId()) {
-                                maxId = r.getId();
+
+                            @Override
+                            public void onFailure(Throwable t) {
+                                Log.w(TAG, "Failed to load scheduled recording.", t);
+                                mPendingDvrFuture.remove(this);
                             }
-                        }
-                        if (!toUpdate.isEmpty()) {
-                            updateScheduledRecording(ScheduledRecording.toArray(toUpdate));
-                        }
-                        if (!toDelete.isEmpty()) {
-                            removeScheduledRecording(ScheduledRecording.toArray(toDelete));
-                        }
-                        IdGenerator.SCHEDULED_RECORDING.setMaxId(maxId);
-                        if (mRecordedProgramLoadFinished) {
-                            validateSeriesRecordings();
-                        }
-                        mDvrLoadFinished = true;
-                        notifyDvrScheduleLoadFinished();
-                        if (isInitialized()) {
-                            mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this);
-                            mDbSync.start();
-                            SeriesRecordingScheduler.getInstance(mContext).start();
-                        }
-                    }
-                };
-        dvrQueryScheduleTask.executeOnDbThread();
-        mPendingTasks.add(dvrQueryScheduleTask);
-        RecordedProgramsQueryTask mRecordedProgramQueryTask =
-                new RecordedProgramsQueryTask(mContext.getContentResolver(), null);
+                        });
+        mPendingDvrFuture.add(dvrQueryScheduleFuture);
+        RecordedProgramsQueryTask mRecordedProgramQueryTask = new RecordedProgramsQueryTask(null);
         mRecordedProgramQueryTask.executeOnDbThread();
         ContentResolver cr = mContext.getContentResolver();
         cr.registerContentObserver(RecordedPrograms.CONTENT_URI, true, mContentObserver);
@@ -329,6 +356,12 @@
             i.remove();
             task.cancel(true);
         }
+        Iterator<Future> id = mPendingDvrFuture.iterator();
+        while (id.hasNext()) {
+            Future future = id.next();
+            id.remove();
+            future.cancel(true);
+        }
     }
 
     private void onRecordedProgramsLoadedFinished(Uri uri, List<RecordedProgram> recordedPrograms) {
@@ -607,7 +640,10 @@
         if (mDvrLoadFinished) {
             notifyScheduledRecordingAdded(schedules);
         }
-        new AsyncAddScheduleTask(mContext).executeOnDbThread(schedules);
+        ListenableFuture addScheduleFuture =
+                new AddScheduleFuture(mContext)
+                        .executeOnDbThread(removeFromSetOnCompletion, schedules);
+        mNoStopFuture.add(addScheduleFuture);
         removeDeletedSchedules(schedules);
     }
 
@@ -626,7 +662,10 @@
         if (mDvrLoadFinished) {
             notifySeriesRecordingAdded(seriesRecordings);
         }
-        new AsyncAddSeriesRecordingTask(mContext).executeOnDbThread(seriesRecordings);
+        ListenableFuture addSeriesRecordingFuture =
+                new AddSeriesRecordingFuture(mContext)
+                        .executeOnDbThread(removeFromSetOnCompletion, seriesRecordings);
+        mNoStopFuture.add(addSeriesRecordingFuture);
     }
 
     @Override
@@ -683,12 +722,20 @@
             }
         }
         if (!schedulesToDelete.isEmpty()) {
-            new AsyncDeleteScheduleTask(mContext)
-                    .executeOnDbThread(ScheduledRecording.toArray(schedulesToDelete));
+            ListenableFuture deleteScheduleFuture =
+                    new DeleteScheduleFuture(mContext)
+                            .executeOnDbThread(
+                                    removeFromSetOnCompletion,
+                                    ScheduledRecording.toArray(schedulesToDelete));
+            mNoStopFuture.add(deleteScheduleFuture);
         }
         if (!schedulesNotToDelete.isEmpty()) {
-            new AsyncUpdateScheduleTask(mContext)
-                    .executeOnDbThread(ScheduledRecording.toArray(schedulesNotToDelete));
+            ListenableFuture updateScheduleFuture =
+                    new UpdateScheduleFuture(mContext)
+                            .executeOnDbThread(
+                                    removeFromSetOnCompletion,
+                                    ScheduledRecording.toArray(schedulesNotToDelete));
+            mNoStopFuture.add(updateScheduleFuture);
         }
     }
 
@@ -726,7 +773,10 @@
         if (mDvrLoadFinished) {
             notifySeriesRecordingRemoved(seriesRecordings);
         }
-        new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread(seriesRecordings);
+        ListenableFuture deleteSeriesRecordingFuture =
+                new DeleteSeriesRecordingFuture(mContext)
+                        .executeOnDbThread(removeFromSetOnCompletion, seriesRecordings);
+        mNoStopFuture.add(deleteSeriesRecordingFuture);
         removeDeletedSchedules(seriesRecordings);
     }
 
@@ -778,7 +828,10 @@
             notifyScheduledRecordingStatusChanged(scheduleArray);
         }
         if (updateDb) {
-            new AsyncUpdateScheduleTask(mContext).executeOnDbThread(scheduleArray);
+            ListenableFuture updateScheduleFuture =
+                    new UpdateScheduleFuture(mContext)
+                            .executeOnDbThread(removeFromSetOnCompletion, scheduleArray);
+            mNoStopFuture.add(updateScheduleFuture);
         }
         checkAndRemoveEmptySeriesRecording(seriesRecordingIdsToCheck);
         removeDeletedSchedules(schedules);
@@ -802,7 +855,10 @@
         if (mDvrLoadFinished) {
             notifySeriesRecordingChanged(seriesRecordings);
         }
-        new AsyncUpdateSeriesRecordingTask(mContext).executeOnDbThread(seriesRecordings);
+        ListenableFuture updateSeriesRecordingFuture =
+                new UpdateSeriesRecordingFuture(mContext)
+                        .executeOnDbThread(removeFromSetOnCompletion, seriesRecordings);
+        mNoStopFuture.add(updateSeriesRecordingFuture);
     }
 
     private boolean isInputAvailable(String inputId) {
@@ -820,8 +876,12 @@
             }
         }
         if (!schedulesToDelete.isEmpty()) {
-            new AsyncDeleteScheduleTask(mContext)
-                    .executeOnDbThread(ScheduledRecording.toArray(schedulesToDelete));
+            ListenableFuture deleteScheduleFuture =
+                    new DeleteScheduleFuture(mContext)
+                            .executeOnDbThread(
+                                    removeFromSetOnCompletion,
+                                    ScheduledRecording.toArray(schedulesToDelete));
+            mNoStopFuture.add(deleteScheduleFuture);
         }
     }
 
@@ -841,8 +901,12 @@
             }
         }
         if (!schedulesToDelete.isEmpty()) {
-            new AsyncDeleteScheduleTask(mContext)
-                    .executeOnDbThread(ScheduledRecording.toArray(schedulesToDelete));
+            ListenableFuture deleteScheduleFuture =
+                    new DeleteScheduleFuture(mContext)
+                            .executeOnDbThread(
+                                    removeFromSetOnCompletion,
+                                    ScheduledRecording.toArray(schedulesToDelete));
+            mNoStopFuture.add(deleteScheduleFuture);
         }
     }
 
@@ -852,38 +916,25 @@
                 moveElements(
                         mScheduledRecordingsForRemovedInput,
                         mScheduledRecordings,
-                        new Filter<ScheduledRecording>() {
-                            @Override
-                            public boolean filter(ScheduledRecording r) {
-                                return r.getInputId().equals(inputId);
-                            }
-                        });
+                        r -> r.getInputId().equals(inputId));
         List<RecordedProgram> movedRecordedPrograms =
                 moveElements(
                         mRecordedProgramsForRemovedInput,
                         mRecordedPrograms,
-                        new Filter<RecordedProgram>() {
-                            @Override
-                            public boolean filter(RecordedProgram r) {
-                                return r.getInputId().equals(inputId);
-                            }
-                        });
+                        r -> r.getInputId().equals(inputId));
         List<SeriesRecording> removedSeriesRecordings = new ArrayList<>();
         List<SeriesRecording> movedSeriesRecordings =
                 moveElements(
                         mSeriesRecordingsForRemovedInput,
                         mSeriesRecordings,
-                        new Filter<SeriesRecording>() {
-                            @Override
-                            public boolean filter(SeriesRecording r) {
-                                if (r.getInputId().equals(inputId)) {
-                                    if (!isEmptySeriesRecording(r)) {
-                                        return true;
-                                    }
-                                    removedSeriesRecordings.add(r);
+                        r -> {
+                            if (r.getInputId().equals(inputId)) {
+                                if (!isEmptySeriesRecording(r)) {
+                                    return true;
                                 }
-                                return false;
+                                removedSeriesRecordings.add(r);
                             }
+                            return false;
                         });
         if (!movedSchedules.isEmpty()) {
             for (ScheduledRecording schedule : movedSchedules) {
@@ -898,8 +949,12 @@
         for (SeriesRecording r : removedSeriesRecordings) {
             mSeriesRecordingsForRemovedInput.remove(r.getId());
         }
-        new AsyncDeleteSeriesRecordingTask(mContext)
-                .executeOnDbThread(SeriesRecording.toArray(removedSeriesRecordings));
+        ListenableFuture deleteSeriesRecordingFuture =
+                new DeleteSeriesRecordingFuture(mContext)
+                        .executeOnDbThread(
+                                removeFromSetOnCompletion,
+                                SeriesRecording.toArray(removedSeriesRecordings));
+        mNoStopFuture.add(deleteSeriesRecordingFuture);
         // Notify after all the data are moved.
         if (!movedSchedules.isEmpty()) {
             notifyScheduledRecordingAdded(ScheduledRecording.toArray(movedSchedules));
@@ -918,32 +973,17 @@
                 moveElements(
                         mScheduledRecordings,
                         mScheduledRecordingsForRemovedInput,
-                        new Filter<ScheduledRecording>() {
-                            @Override
-                            public boolean filter(ScheduledRecording r) {
-                                return r.getInputId().equals(inputId);
-                            }
-                        });
+                        r -> r.getInputId().equals(inputId));
         List<SeriesRecording> movedSeriesRecordings =
                 moveElements(
                         mSeriesRecordings,
                         mSeriesRecordingsForRemovedInput,
-                        new Filter<SeriesRecording>() {
-                            @Override
-                            public boolean filter(SeriesRecording r) {
-                                return r.getInputId().equals(inputId);
-                            }
-                        });
+                        r -> r.getInputId().equals(inputId));
         List<RecordedProgram> movedRecordedPrograms =
                 moveElements(
                         mRecordedPrograms,
                         mRecordedProgramsForRemovedInput,
-                        new Filter<RecordedProgram>() {
-                            @Override
-                            public boolean filter(RecordedProgram r) {
-                                return r.getInputId().equals(inputId);
-                            }
-                        });
+                        r -> r.getInputId().equals(inputId));
         if (!movedSchedules.isEmpty()) {
             for (ScheduledRecording schedule : movedSchedules) {
                 mProgramId2ScheduledRecordings.remove(schedule.getProgramId());
@@ -1002,10 +1042,18 @@
                 i.remove();
             }
         }
-        new AsyncDeleteScheduleTask(mContext)
-                .executeOnDbThread(ScheduledRecording.toArray(schedulesToDelete));
-        new AsyncDeleteSeriesRecordingTask(mContext)
-                .executeOnDbThread(SeriesRecording.toArray(seriesRecordingsToDelete));
+        ListenableFuture deleteScheduleFuture =
+                new DeleteScheduleFuture(mContext)
+                        .executeOnDbThread(
+                                removeFromSetOnCompletion,
+                                ScheduledRecording.toArray(schedulesToDelete));
+        mNoStopFuture.add(deleteScheduleFuture);
+        ListenableFuture deleteSeriesRecordingFuture =
+                new DeleteSeriesRecordingFuture(mContext)
+                        .executeOnDbThread(
+                                removeFromSetOnCompletion,
+                                SeriesRecording.toArray(seriesRecordingsToDelete));
+        mNoStopFuture.add(deleteSeriesRecordingFuture);
         new AsyncDbTask<Void, Void, Void>(mDbExecutor) {
             @Override
             protected Void doInBackground(Void... params) {
@@ -1036,7 +1084,10 @@
         }
         if (!removedSeriesRecordings.isEmpty()) {
             SeriesRecording[] removed = SeriesRecording.toArray(removedSeriesRecordings);
-            new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread(removed);
+            ListenableFuture deleteSeriesRecordingFuture =
+                    new DeleteSeriesRecordingFuture(mContext)
+                            .executeOnDbThread(removeFromSetOnCompletion, removed);
+            mNoStopFuture.add(deleteSeriesRecordingFuture);
             if (mDvrLoadFinished) {
                 notifySeriesRecordingRemoved(removed);
             }
@@ -1046,8 +1097,8 @@
     private final class RecordedProgramsQueryTask extends AsyncRecordedProgramQueryTask {
         private final Uri mUri;
 
-        public RecordedProgramsQueryTask(ContentResolver contentResolver, Uri uri) {
-            super(mDbExecutor, contentResolver, uri == null ? RecordedPrograms.CONTENT_URI : uri);
+        public RecordedProgramsQueryTask(Uri uri) {
+            super(mDbExecutor, mContext, uri == null ? RecordedPrograms.CONTENT_URI : uri);
             mUri = uri;
         }
 
diff --git a/src/com/android/tv/dvr/DvrManager.java b/src/com/android/tv/dvr/DvrManager.java
index 63a245a..cc9ad37 100644
--- a/src/com/android/tv/dvr/DvrManager.java
+++ b/src/com/android/tv/dvr/DvrManager.java
@@ -29,6 +29,7 @@
 import android.os.Build;
 import android.os.Handler;
 import android.os.RemoteException;
+import android.support.annotation.AnyThread;
 import android.support.annotation.MainThread;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
@@ -441,14 +442,7 @@
         }
         synchronized (mListener) {
             for (final Entry<Listener, Handler> entry : mListener.entrySet()) {
-                entry.getValue()
-                        .post(
-                                new Runnable() {
-                                    @Override
-                                    public void run() {
-                                        entry.getKey().onStopRecordingRequested(recording);
-                                    }
-                                });
+                entry.getValue().post(() -> entry.getKey().onStopRecordingRequested(recording));
             }
         }
     }
@@ -484,26 +478,26 @@
     }
 
     /** Removes the recorded program. It deletes the file if possible. */
-    public void removeRecordedProgram(Uri recordedProgramUri) {
+    public void removeRecordedProgram(Uri recordedProgramUri, boolean deleteFile) {
         if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
             return;
         }
-        removeRecordedProgram(ContentUris.parseId(recordedProgramUri));
+        removeRecordedProgram(ContentUris.parseId(recordedProgramUri), deleteFile);
     }
 
     /** Removes the recorded program. It deletes the file if possible. */
-    public void removeRecordedProgram(long recordedProgramId) {
+    public void removeRecordedProgram(long recordedProgramId, boolean deleteFile) {
         if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
             return;
         }
         RecordedProgram recordedProgram = mDataManager.getRecordedProgram(recordedProgramId);
         if (recordedProgram != null) {
-            removeRecordedProgram(recordedProgram);
+            removeRecordedProgram(recordedProgram, deleteFile);
         }
     }
 
     /** Removes the recorded program. It deletes the file if possible. */
-    public void removeRecordedProgram(final RecordedProgram recordedProgram) {
+    public void removeRecordedProgram(final RecordedProgram recordedProgram, boolean deleteFile) {
         if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
             return;
         }
@@ -516,7 +510,7 @@
 
             @Override
             protected void onPostExecute(Integer deletedCounts) {
-                if (deletedCounts > 0) {
+                if (deletedCounts > 0 && deleteFile) {
                     new AsyncTask<Void, Void, Void>() {
                         @Override
                         protected Void doInBackground(Void... params) {
@@ -529,7 +523,7 @@
         }.executeOnDbThread();
     }
 
-    public void removeRecordedPrograms(List<Long> recordedProgramIds) {
+    public void removeRecordedPrograms(List<Long> recordedProgramIds, boolean deleteFiles) {
         final ArrayList<ContentProviderOperation> dbOperations = new ArrayList<>();
         final List<Uri> dataUris = new ArrayList<>();
         for (Long rId : recordedProgramIds) {
@@ -554,7 +548,7 @@
 
             @Override
             protected void onPostExecute(Boolean success) {
-                if (success) {
+                if (success && deleteFiles) {
                     new AsyncTask<Void, Void, Void>() {
                         @Override
                         protected Void doInBackground(Void... params) {
@@ -829,37 +823,47 @@
     @WorkerThread
     private void removeRecordedData(Uri dataUri) {
         try {
-            if (dataUri != null
-                    && ContentResolver.SCHEME_FILE.equals(dataUri.getScheme())
-                    && dataUri.getPath() != null) {
+            if (isFile(dataUri)) {
                 File recordedProgramPath = new File(dataUri.getPath());
                 if (!recordedProgramPath.exists()) {
                     if (DEBUG) Log.d(TAG, "File to delete not exist: " + recordedProgramPath);
                 } else {
-                    CommonUtils.deleteDirOrFile(recordedProgramPath);
-                    if (DEBUG) {
-                        Log.d(TAG, "Sucessfully deleted files of the recorded program: " + dataUri);
+                    if (CommonUtils.deleteDirOrFile(recordedProgramPath)) {
+                        if (DEBUG) {
+                            Log.d(
+                                    TAG,
+                                    "Successfully deleted files of the recorded program: "
+                                            + dataUri);
+                        }
+                    } else {
+                        Log.w(TAG, "Unable to delete recording data at " + dataUri);
                     }
                 }
             }
         } catch (SecurityException e) {
-            if (DEBUG) {
-                Log.d(
-                        TAG,
-                        "To delete this recorded program, please manually delete video data at"
-                                + "\nadb shell rm -rf "
-                                + dataUri);
-            }
+            Log.w(TAG, "Unable to delete recording data at " + dataUri, e);
         }
     }
 
+    @AnyThread
+    public static boolean isFromBundledInput(RecordedProgram mRecordedProgram) {
+        return CommonUtils.isInBundledPackageSet(mRecordedProgram.getPackageName());
+    }
+
+    @AnyThread
+    public static boolean isFile(Uri dataUri) {
+        return dataUri != null
+                && ContentResolver.SCHEME_FILE.equals(dataUri.getScheme())
+                && dataUri.getPath() != null;
+    }
+
     /**
      * Remove all the records related to the input.
      *
      * <p>Note that this should be called after the input was removed.
      */
     public void forgetStorage(String inputId) {
-        if (mDataManager.isInitialized()) {
+        if (mDataManager != null && mDataManager.isInitialized()) {
             mDataManager.forgetStorage(inputId);
         }
     }
diff --git a/src/com/android/tv/dvr/DvrScheduleManager.java b/src/com/android/tv/dvr/DvrScheduleManager.java
index d5126b1..7202dce 100644
--- a/src/com/android/tv/dvr/DvrScheduleManager.java
+++ b/src/com/android/tv/dvr/DvrScheduleManager.java
@@ -923,12 +923,8 @@
         List<ConflictInfo> result = new ArrayList<>(conflicts.values());
         Collections.sort(
                 result,
-                new Comparator<ConflictInfo>() {
-                    @Override
-                    public int compare(ConflictInfo lhs, ConflictInfo rhs) {
-                        return RESULT_COMPARATOR.compare(lhs.schedule, rhs.schedule);
-                    }
-                });
+                (ConflictInfo lhs, ConflictInfo rhs) ->
+                        RESULT_COMPARATOR.compare(lhs.schedule, rhs.schedule));
         return result;
     }
 
diff --git a/src/com/android/tv/dvr/DvrStorageStatusManager.java b/src/com/android/tv/dvr/DvrStorageStatusManager.java
index ed8d690..dc347a9 100644
--- a/src/com/android/tv/dvr/DvrStorageStatusManager.java
+++ b/src/com/android/tv/dvr/DvrStorageStatusManager.java
@@ -24,8 +24,9 @@
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.RemoteException;
-import android.support.media.tv.TvContractCompat;
+import android.support.annotation.Nullable;
 import android.util.Log;
+import androidx.tvprovider.media.tv.TvContractCompat;
 import com.android.tv.TvSingletons;
 import com.android.tv.common.recording.RecordingStorageStatusManager;
 import com.android.tv.common.util.CommonUtils;
@@ -123,6 +124,8 @@
             }
         }
 
+
+        @Nullable
         private List<ContentProviderOperation> getDeleteOps() {
             List<ContentProviderOperation> ops = new ArrayList<>();
 
@@ -165,6 +168,9 @@
                     }
                 }
                 return ops;
+            } catch (Exception e) {
+                Log.w(TAG, "Error when getting delete ops at CleanUpDbTask", e);
+                return null;
             }
         }
     }
diff --git a/src/com/android/tv/dvr/DvrTvView.java b/src/com/android/tv/dvr/DvrTvView.java
new file mode 100644
index 0000000..be1f418
--- /dev/null
+++ b/src/com/android/tv/dvr/DvrTvView.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.dvr;
+
+import android.content.Context;
+import android.media.PlaybackParams;
+import android.media.session.PlaybackState;
+import android.media.tv.TvTrackInfo;
+import android.media.tv.TvView;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import com.android.tv.InputSessionManager;
+import com.android.tv.InputSessionManager.TvViewSession;
+import com.android.tv.TvSingletons;
+import com.android.tv.common.compat.TvViewCompat.TvInputCallbackCompat;
+import com.android.tv.dvr.ui.playback.DvrPlayer;
+import com.android.tv.ui.AppLayerTvView;
+import com.android.tv.ui.api.TunableTvViewPlayingApi;
+import java.util.List;
+
+/**
+ * A {@link TvView} wrapper to handle events and TvView session.
+ */
+public class DvrTvView implements TunableTvViewPlayingApi {
+
+    private final AppLayerTvView mTvView;
+    private DvrPlayer mDvrPlayer;
+    private String mInputId;
+    private Uri mRecordedProgramUri;
+    private TvInputCallbackCompat mTvInputCallbackCompat;
+    private InputSessionManager mInputSessionManager;
+    private TvViewSession mSession;
+
+    public DvrTvView(Context context, AppLayerTvView tvView, DvrPlayer player) {
+        mTvView = tvView;
+        mDvrPlayer = player;
+        mInputSessionManager = TvSingletons.getSingletons(context).getInputSessionManager();
+    }
+
+    @Override
+    public boolean isPlaying() {
+        return mDvrPlayer.getPlaybackState() == PlaybackState.STATE_PLAYING;
+    }
+
+    @Override
+    public void setStreamVolume(float volume) {
+        mTvView.setStreamVolume(volume);
+    }
+
+    @Override
+    public void setTimeShiftListener(TimeShiftListener listener) {
+        // TimeShiftListener is never called from DvrTvView because TimeShift is always available
+        // and onRecordStartTimeChanged is not called during playback.
+    }
+
+    @Override
+    public boolean isTimeShiftAvailable() {
+        return true;
+    }
+
+    @Override
+    public void timeShiftPlay() {
+        if (mInputId != null && mRecordedProgramUri != null) {
+            mTvView.timeShiftPlay(mInputId, mRecordedProgramUri);
+        }
+    }
+
+    public void timeShiftPlay(String inputId, Uri recordedProgramUri) {
+        mInputId = inputId;
+        mRecordedProgramUri = recordedProgramUri;
+        mSession.timeShiftPlay(inputId, recordedProgramUri);
+    }
+
+    @Override
+    public void timeShiftPause() {
+        mTvView.timeShiftPause();
+    }
+
+    @Override
+    public void timeShiftRewind(int speed) {
+        PlaybackParams params = new PlaybackParams();
+        params.setSpeed(speed * -1);
+        mTvView.timeShiftSetPlaybackParams(params);
+    }
+
+    @Override
+    public void timeShiftFastForward(int speed) {
+        PlaybackParams params = new PlaybackParams();
+        params.setSpeed(speed);
+        mTvView.timeShiftSetPlaybackParams(params);
+    }
+
+    @Override
+    public void timeShiftSeekTo(long timeMs) {
+        mTvView.timeShiftSeekTo(timeMs);
+    }
+
+    @Override
+    public long timeShiftGetCurrentPositionMs() {
+        return mDvrPlayer.getPlaybackPosition();
+    }
+
+    public void setCaptionEnabled(boolean enabled) {
+        mTvView.setCaptionEnabled(enabled);
+    }
+
+    public void timeShiftResume() {
+        mTvView.timeShiftResume();
+    }
+
+    public void reset() {
+        mSession.reset();
+    }
+
+    public List<TvTrackInfo> getTracks(int type) {
+        return mTvView.getTracks(type);
+    }
+
+    public void selectTrack(int type, String trackId) {
+        mTvView.selectTrack(type, trackId);
+    }
+
+    public void timeShiftSetPlaybackParams(PlaybackParams params) {
+        mTvView.timeShiftSetPlaybackParams(params);
+    }
+
+    public void setTimeShiftPositionCallback(@Nullable TvView.TimeShiftPositionCallback callback) {
+        mTvView.setTimeShiftPositionCallback(callback);
+    }
+
+    public void setCallback(@Nullable TvInputCallbackCompat callback) {
+        mTvInputCallbackCompat = callback;
+        mTvView.setCallback(callback);
+    }
+
+    public void init() {
+        mSession = mInputSessionManager.createTvViewSession(mTvView, this, mTvInputCallbackCompat);
+    }
+
+    public void release() {
+        mInputSessionManager.releaseTvViewSession(mSession);
+        mInputSessionManager = null;
+        mDvrPlayer = null;
+    }
+}
diff --git a/src/com/android/tv/dvr/data/RecordedProgram.java b/src/com/android/tv/dvr/data/RecordedProgram.java
index e1fbca8..899e65a 100644
--- a/src/com/android/tv/dvr/data/RecordedProgram.java
+++ b/src/com/android/tv/dvr/data/RecordedProgram.java
@@ -22,31 +22,38 @@
 import android.content.Context;
 import android.database.Cursor;
 import android.media.tv.TvContentRating;
-import android.media.tv.TvContract;
+import android.media.tv.TvContract.Programs.Genres;
 import android.media.tv.TvContract.RecordedPrograms;
 import android.net.Uri;
 import android.os.Build;
+import android.support.annotation.CheckResult;
 import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
 import android.text.TextUtils;
+import android.util.Log;
 import com.android.tv.common.R;
 import com.android.tv.common.TvContentRatingCache;
+import com.android.tv.common.data.RecordedProgramState;
 import com.android.tv.common.util.CommonUtils;
+import com.android.tv.common.util.StringUtils;
 import com.android.tv.data.BaseProgram;
 import com.android.tv.data.GenreItems;
 import com.android.tv.data.InternalDataUtils;
-import java.util.Arrays;
+import com.android.tv.util.TvProviderUtils;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
 import java.util.Collection;
 import java.util.Comparator;
-import java.util.Objects;
 import java.util.concurrent.TimeUnit;
 
 /** Immutable instance of {@link android.media.tv.TvContract.RecordedPrograms}. */
 @TargetApi(Build.VERSION_CODES.N)
-public class RecordedProgram extends BaseProgram {
+@AutoValue
+public abstract class RecordedProgram extends BaseProgram {
     public static final int ID_NOT_SET = -1;
+    private static final String TAG = "RecordedProgram";
 
     public static final String[] PROJECTION = {
-        // These are in exactly the order listed in RecordedPrograms
         RecordedPrograms._ID,
         RecordedPrograms.COLUMN_PACKAGE_NAME,
         RecordedPrograms.COLUMN_INPUT_ID,
@@ -73,10 +80,6 @@
         RecordedPrograms.COLUMN_RECORDING_DATA_BYTES,
         RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS,
         RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS,
-        RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1,
-        RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2,
-        RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3,
-        RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4,
         RecordedPrograms.COLUMN_VERSION_NUMBER,
         RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA,
     };
@@ -89,834 +92,372 @@
                         .setPackageName(cursor.getString(index++))
                         .setInputId(cursor.getString(index++))
                         .setChannelId(cursor.getLong(index++))
-                        .setTitle(cursor.getString(index++))
-                        .setSeasonNumber(cursor.getString(index++))
-                        .setSeasonTitle(cursor.getString(index++))
-                        .setEpisodeNumber(cursor.getString(index++))
-                        .setEpisodeTitle(cursor.getString(index++))
+                        .setTitle(StringUtils.nullToEmpty(cursor.getString(index++)))
+                        .setSeasonNumber(StringUtils.nullToEmpty(cursor.getString(index++)))
+                        .setSeasonTitle(StringUtils.nullToEmpty(cursor.getString(index++)))
+                        .setEpisodeNumber(StringUtils.nullToEmpty(cursor.getString(index++)))
+                        .setEpisodeTitle(StringUtils.nullToEmpty(cursor.getString(index++)))
                         .setStartTimeUtcMillis(cursor.getLong(index++))
                         .setEndTimeUtcMillis(cursor.getLong(index++))
                         .setBroadcastGenres(cursor.getString(index++))
                         .setCanonicalGenres(cursor.getString(index++))
-                        .setShortDescription(cursor.getString(index++))
-                        .setLongDescription(cursor.getString(index++))
+                        .setDescription(StringUtils.nullToEmpty(cursor.getString(index++)))
+                        .setLongDescription(StringUtils.nullToEmpty(cursor.getString(index++)))
                         .setVideoWidth(cursor.getInt(index++))
                         .setVideoHeight(cursor.getInt(index++))
-                        .setAudioLanguage(cursor.getString(index++))
+                        .setAudioLanguage(StringUtils.nullToEmpty(cursor.getString(index++)))
                         .setContentRatings(
                                 TvContentRatingCache.getInstance()
                                         .getRatings(cursor.getString(index++)))
-                        .setPosterArtUri(cursor.getString(index++))
-                        .setThumbnailUri(cursor.getString(index++))
+                        .setPosterArtUri(StringUtils.nullToEmpty(cursor.getString(index++)))
+                        .setThumbnailUri(StringUtils.nullToEmpty(cursor.getString(index++)))
                         .setSearchable(cursor.getInt(index++) == 1)
                         .setDataUri(cursor.getString(index++))
                         .setDataBytes(cursor.getLong(index++))
                         .setDurationMillis(cursor.getLong(index++))
                         .setExpireTimeUtcMillis(cursor.getLong(index++))
-                        .setInternalProviderFlag1(cursor.getInt(index++))
-                        .setInternalProviderFlag2(cursor.getInt(index++))
-                        .setInternalProviderFlag3(cursor.getInt(index++))
-                        .setInternalProviderFlag4(cursor.getInt(index++))
                         .setVersionNumber(cursor.getInt(index++));
-        if (CommonUtils.isInBundledPackageSet(builder.mPackageName)) {
+        if (CommonUtils.isInBundledPackageSet(builder.getPackageName())) {
             InternalDataUtils.deserializeInternalProviderData(cursor.getBlob(index), builder);
         }
+        index++;
+        if (TvProviderUtils.getRecordedProgramHasSeriesIdColumn()) {
+            builder.setSeriesId(StringUtils.nullToEmpty(cursor.getString(index++)));
+        }
+        if (TvProviderUtils.getRecordedProgramHasStateColumn()) {
+            builder.setState(cursor.getString(index++));
+        }
         return builder.build();
     }
 
-    public static ContentValues toValues(RecordedProgram recordedProgram) {
+    @WorkerThread
+    public static ContentValues toValues(Context context, RecordedProgram recordedProgram) {
         ContentValues values = new ContentValues();
-        if (recordedProgram.mId != ID_NOT_SET) {
-            values.put(RecordedPrograms._ID, recordedProgram.mId);
+        if (recordedProgram.getId() != ID_NOT_SET) {
+            values.put(RecordedPrograms._ID, recordedProgram.getId());
         }
-        values.put(RecordedPrograms.COLUMN_INPUT_ID, recordedProgram.mInputId);
-        values.put(RecordedPrograms.COLUMN_CHANNEL_ID, recordedProgram.mChannelId);
-        values.put(RecordedPrograms.COLUMN_TITLE, recordedProgram.mTitle);
-        values.put(RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER, recordedProgram.mSeasonNumber);
-        values.put(RecordedPrograms.COLUMN_SEASON_TITLE, recordedProgram.mSeasonTitle);
-        values.put(RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER, recordedProgram.mEpisodeNumber);
-        values.put(RecordedPrograms.COLUMN_EPISODE_TITLE, recordedProgram.mTitle);
+        values.put(RecordedPrograms.COLUMN_INPUT_ID, recordedProgram.getInputId());
+        values.put(RecordedPrograms.COLUMN_CHANNEL_ID, recordedProgram.getChannelId());
+        values.put(RecordedPrograms.COLUMN_TITLE, recordedProgram.getTitle());
         values.put(
-                RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, recordedProgram.mStartTimeUtcMillis);
-        values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, recordedProgram.mEndTimeUtcMillis);
+                RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER, recordedProgram.getSeasonNumber());
+        values.put(RecordedPrograms.COLUMN_SEASON_TITLE, recordedProgram.getSeasonTitle());
+        values.put(
+                RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER, recordedProgram.getEpisodeNumber());
+        values.put(RecordedPrograms.COLUMN_EPISODE_TITLE, recordedProgram.getEpisodeTitle());
+        values.put(
+                RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS,
+                recordedProgram.getStartTimeUtcMillis());
+        values.put(
+                RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, recordedProgram.getEndTimeUtcMillis());
         values.put(
                 RecordedPrograms.COLUMN_BROADCAST_GENRE,
-                safeEncode(recordedProgram.mBroadcastGenres));
+                safeEncode(recordedProgram.getBroadcastGenres()));
         values.put(
                 RecordedPrograms.COLUMN_CANONICAL_GENRE,
-                safeEncode(recordedProgram.mCanonicalGenres));
-        values.put(RecordedPrograms.COLUMN_SHORT_DESCRIPTION, recordedProgram.mShortDescription);
-        values.put(RecordedPrograms.COLUMN_LONG_DESCRIPTION, recordedProgram.mLongDescription);
-        if (recordedProgram.mVideoWidth == 0) {
+                safeEncode(recordedProgram.getCanonicalGenres()));
+        values.put(RecordedPrograms.COLUMN_SHORT_DESCRIPTION, recordedProgram.getDescription());
+        values.put(RecordedPrograms.COLUMN_LONG_DESCRIPTION, recordedProgram.getLongDescription());
+        if (recordedProgram.getVideoWidth() == 0) {
             values.putNull(RecordedPrograms.COLUMN_VIDEO_WIDTH);
         } else {
-            values.put(RecordedPrograms.COLUMN_VIDEO_WIDTH, recordedProgram.mVideoWidth);
+            values.put(RecordedPrograms.COLUMN_VIDEO_WIDTH, recordedProgram.getVideoWidth());
         }
-        if (recordedProgram.mVideoHeight == 0) {
+        if (recordedProgram.getVideoHeight() == 0) {
             values.putNull(RecordedPrograms.COLUMN_VIDEO_HEIGHT);
         } else {
-            values.put(RecordedPrograms.COLUMN_VIDEO_HEIGHT, recordedProgram.mVideoHeight);
+            values.put(RecordedPrograms.COLUMN_VIDEO_HEIGHT, recordedProgram.getVideoHeight());
         }
-        values.put(RecordedPrograms.COLUMN_AUDIO_LANGUAGE, recordedProgram.mAudioLanguage);
+        values.put(RecordedPrograms.COLUMN_AUDIO_LANGUAGE, recordedProgram.getAudioLanguage());
         values.put(
                 RecordedPrograms.COLUMN_CONTENT_RATING,
-                TvContentRatingCache.contentRatingsToString(recordedProgram.mContentRatings));
-        values.put(RecordedPrograms.COLUMN_POSTER_ART_URI, recordedProgram.mPosterArtUri);
-        values.put(RecordedPrograms.COLUMN_THUMBNAIL_URI, recordedProgram.mThumbnailUri);
-        values.put(RecordedPrograms.COLUMN_SEARCHABLE, recordedProgram.mSearchable ? 1 : 0);
+                TvContentRatingCache.contentRatingsToString(recordedProgram.getContentRatings()));
+        values.put(RecordedPrograms.COLUMN_POSTER_ART_URI, recordedProgram.getPosterArtUri());
+        values.put(RecordedPrograms.COLUMN_THUMBNAIL_URI, recordedProgram.getThumbnailUri());
+        values.put(RecordedPrograms.COLUMN_SEARCHABLE, recordedProgram.isSearchable() ? 1 : 0);
         values.put(
-                RecordedPrograms.COLUMN_RECORDING_DATA_URI, safeToString(recordedProgram.mDataUri));
-        values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, recordedProgram.mDataBytes);
+                RecordedPrograms.COLUMN_RECORDING_DATA_URI,
+                safeToString(recordedProgram.getDataUri()));
+        values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, recordedProgram.getDataBytes());
         values.put(
-                RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, recordedProgram.mDurationMillis);
+                RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS,
+                recordedProgram.getDurationMillis());
         values.put(
                 RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS,
-                recordedProgram.mExpireTimeUtcMillis);
+                recordedProgram.getExpireTimeUtcMillis());
         values.put(
                 RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA,
                 InternalDataUtils.serializeInternalProviderData(recordedProgram));
-        values.put(
-                RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1,
-                recordedProgram.mInternalProviderFlag1);
-        values.put(
-                RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2,
-                recordedProgram.mInternalProviderFlag2);
-        values.put(
-                RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3,
-                recordedProgram.mInternalProviderFlag3);
-        values.put(
-                RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4,
-                recordedProgram.mInternalProviderFlag4);
-        values.put(RecordedPrograms.COLUMN_VERSION_NUMBER, recordedProgram.mVersionNumber);
+        values.put(RecordedPrograms.COLUMN_VERSION_NUMBER, recordedProgram.getVersionNumber());
+        if (TvProviderUtils.checkSeriesIdColumn(context, RecordedPrograms.CONTENT_URI)) {
+            values.put(COLUMN_SERIES_ID, recordedProgram.getSeriesId());
+        }
+        if (TvProviderUtils.checkStateColumn(context, RecordedPrograms.CONTENT_URI)) {
+            values.put(COLUMN_STATE, recordedProgram.getState().toString());
+        }
         return values;
     }
 
-    public static class Builder {
-        private long mId = ID_NOT_SET;
-        private String mPackageName;
-        private String mInputId;
-        private long mChannelId;
-        private String mTitle;
-        private String mSeriesId;
-        private String mSeasonNumber;
-        private String mSeasonTitle;
-        private String mEpisodeNumber;
-        private String mEpisodeTitle;
-        private long mStartTimeUtcMillis;
-        private long mEndTimeUtcMillis;
-        private String[] mBroadcastGenres;
-        private String[] mCanonicalGenres;
-        private String mShortDescription;
-        private String mLongDescription;
-        private int mVideoWidth;
-        private int mVideoHeight;
-        private String mAudioLanguage;
-        private TvContentRating[] mContentRatings;
-        private String mPosterArtUri;
-        private String mThumbnailUri;
-        private boolean mSearchable = true;
-        private Uri mDataUri;
-        private long mDataBytes;
-        private long mDurationMillis;
-        private long mExpireTimeUtcMillis;
-        private int mInternalProviderFlag1;
-        private int mInternalProviderFlag2;
-        private int mInternalProviderFlag3;
-        private int mInternalProviderFlag4;
-        private int mVersionNumber;
+    /** Builder for {@link RecordedProgram}s. */
+    @AutoValue.Builder
+    public abstract static class Builder {
 
-        public Builder setId(long id) {
-            mId = id;
-            return this;
-        }
+        public abstract Builder setId(long id);
 
-        public Builder setPackageName(String packageName) {
-            mPackageName = packageName;
-            return this;
-        }
+        public abstract Builder setPackageName(String packageName);
 
-        public Builder setInputId(String inputId) {
-            mInputId = inputId;
-            return this;
-        }
+        abstract String getPackageName();
 
-        public Builder setChannelId(long channelId) {
-            mChannelId = channelId;
-            return this;
-        }
+        public abstract Builder setInputId(String inputId);
 
-        public Builder setTitle(String title) {
-            mTitle = title;
-            return this;
-        }
+        public abstract Builder setChannelId(long channelId);
 
-        public Builder setSeriesId(String seriesId) {
-            mSeriesId = seriesId;
-            return this;
-        }
+        abstract String getTitle();
 
-        public Builder setSeasonNumber(String seasonNumber) {
-            mSeasonNumber = seasonNumber;
-            return this;
-        }
+        public abstract Builder setTitle(String title);
 
-        public Builder setSeasonTitle(String seasonTitle) {
-            mSeasonTitle = seasonTitle;
-            return this;
-        }
+        abstract String getSeriesId();
 
-        public Builder setEpisodeNumber(String episodeNumber) {
-            mEpisodeNumber = episodeNumber;
-            return this;
-        }
+        public abstract Builder setSeriesId(String seriesId);
 
-        public Builder setEpisodeTitle(String episodeTitle) {
-            mEpisodeTitle = episodeTitle;
-            return this;
-        }
+        public abstract Builder setSeasonNumber(String seasonNumber);
 
-        public Builder setStartTimeUtcMillis(long startTimeUtcMillis) {
-            mStartTimeUtcMillis = startTimeUtcMillis;
-            return this;
-        }
+        public abstract Builder setSeasonTitle(String seasonTitle);
 
-        public Builder setEndTimeUtcMillis(long endTimeUtcMillis) {
-            mEndTimeUtcMillis = endTimeUtcMillis;
-            return this;
-        }
+        @Nullable
+        abstract String getEpisodeNumber();
 
-        public Builder setBroadcastGenres(String broadcastGenres) {
-            if (TextUtils.isEmpty(broadcastGenres)) {
-                mBroadcastGenres = null;
-                return this;
+        public abstract Builder setEpisodeNumber(String episodeNumber);
+
+        public abstract Builder setEpisodeTitle(String episodeTitle);
+
+        public abstract Builder setStartTimeUtcMillis(long startTimeUtcMillis);
+
+        public abstract Builder setEndTimeUtcMillis(long endTimeUtcMillis);
+
+        public abstract Builder setState(RecordedProgramState state);
+
+        public Builder setState(@Nullable String state) {
+
+            if (!TextUtils.isEmpty(state)) {
+                try {
+                    return setState(RecordedProgramState.valueOf(state));
+                } catch (IllegalArgumentException e) {
+                    Log.w(TAG, "Unknown recording state " + state, e);
+                }
             }
-            return setBroadcastGenres(TvContract.Programs.Genres.decode(broadcastGenres));
+            return setState(RecordedProgramState.NOT_SET);
         }
 
-        private Builder setBroadcastGenres(String[] broadcastGenres) {
-            mBroadcastGenres = broadcastGenres;
-            return this;
+        public Builder setBroadcastGenres(@Nullable String broadcastGenres) {
+            return setBroadcastGenres(
+                    TextUtils.isEmpty(broadcastGenres)
+                            ? ImmutableList.of()
+                            : ImmutableList.copyOf(Genres.decode(broadcastGenres)));
         }
 
+        public abstract Builder setBroadcastGenres(ImmutableList<String> broadcastGenres);
+
         public Builder setCanonicalGenres(String canonicalGenres) {
-            if (TextUtils.isEmpty(canonicalGenres)) {
-                mCanonicalGenres = null;
-                return this;
-            }
-            return setCanonicalGenres(TvContract.Programs.Genres.decode(canonicalGenres));
+            return setCanonicalGenres(
+                    TextUtils.isEmpty(canonicalGenres)
+                            ? ImmutableList.of()
+                            : ImmutableList.copyOf(Genres.decode(canonicalGenres)));
         }
 
-        private Builder setCanonicalGenres(String[] canonicalGenres) {
-            mCanonicalGenres = canonicalGenres;
-            return this;
-        }
+        public abstract Builder setCanonicalGenres(ImmutableList<String> canonicalGenres);
 
-        public Builder setShortDescription(String shortDescription) {
-            mShortDescription = shortDescription;
-            return this;
-        }
+        public abstract Builder setDescription(String shortDescription);
 
-        public Builder setLongDescription(String longDescription) {
-            mLongDescription = longDescription;
-            return this;
-        }
+        public abstract Builder setLongDescription(String longDescription);
 
-        public Builder setVideoWidth(int videoWidth) {
-            mVideoWidth = videoWidth;
-            return this;
-        }
+        public abstract Builder setVideoWidth(int videoWidth);
 
-        public Builder setVideoHeight(int videoHeight) {
-            mVideoHeight = videoHeight;
-            return this;
-        }
+        public abstract Builder setVideoHeight(int videoHeight);
 
-        public Builder setAudioLanguage(String audioLanguage) {
-            mAudioLanguage = audioLanguage;
-            return this;
-        }
+        public abstract Builder setAudioLanguage(String audioLanguage);
 
-        public Builder setContentRatings(TvContentRating[] contentRatings) {
-            mContentRatings = contentRatings;
-            return this;
-        }
+        public abstract Builder setContentRatings(ImmutableList<TvContentRating> contentRatings);
 
-        private Uri toUri(String uriString) {
+        private Uri toUri(@Nullable String uriString) {
             try {
                 return uriString == null ? null : Uri.parse(uriString);
             } catch (Exception e) {
-                return null;
+                return Uri.EMPTY;
             }
         }
 
-        public Builder setPosterArtUri(String posterArtUri) {
-            mPosterArtUri = posterArtUri;
-            return this;
-        }
+        public abstract Builder setPosterArtUri(String posterArtUri);
 
-        public Builder setThumbnailUri(String thumbnailUri) {
-            mThumbnailUri = thumbnailUri;
-            return this;
-        }
+        public abstract Builder setThumbnailUri(String thumbnailUri);
 
-        public Builder setSearchable(boolean searchable) {
-            mSearchable = searchable;
-            return this;
-        }
+        public abstract Builder setSearchable(boolean searchable);
 
-        public Builder setDataUri(String dataUri) {
+        public Builder setDataUri(@Nullable String dataUri) {
             return setDataUri(toUri(dataUri));
         }
 
-        public Builder setDataUri(Uri dataUri) {
-            mDataUri = dataUri;
-            return this;
-        }
+        public abstract Builder setDataUri(Uri dataUri);
 
-        public Builder setDataBytes(long dataBytes) {
-            mDataBytes = dataBytes;
-            return this;
-        }
+        public abstract Builder setDataBytes(long dataBytes);
 
-        public Builder setDurationMillis(long durationMillis) {
-            mDurationMillis = durationMillis;
-            return this;
-        }
+        public abstract Builder setDurationMillis(long durationMillis);
 
-        public Builder setExpireTimeUtcMillis(long expireTimeUtcMillis) {
-            mExpireTimeUtcMillis = expireTimeUtcMillis;
-            return this;
-        }
+        public abstract Builder setExpireTimeUtcMillis(long expireTimeUtcMillis);
 
-        public Builder setInternalProviderFlag1(int internalProviderFlag1) {
-            mInternalProviderFlag1 = internalProviderFlag1;
-            return this;
-        }
+        public abstract Builder setVersionNumber(int versionNumber);
 
-        public Builder setInternalProviderFlag2(int internalProviderFlag2) {
-            mInternalProviderFlag2 = internalProviderFlag2;
-            return this;
-        }
-
-        public Builder setInternalProviderFlag3(int internalProviderFlag3) {
-            mInternalProviderFlag3 = internalProviderFlag3;
-            return this;
-        }
-
-        public Builder setInternalProviderFlag4(int internalProviderFlag4) {
-            mInternalProviderFlag4 = internalProviderFlag4;
-            return this;
-        }
-
-        public Builder setVersionNumber(int versionNumber) {
-            mVersionNumber = versionNumber;
-            return this;
-        }
+        abstract RecordedProgram autoBuild();
 
         public RecordedProgram build() {
-            if (TextUtils.isEmpty(mTitle)) {
+            if (TextUtils.isEmpty(getTitle())) {
                 // If title is null, series cannot be generated for this program.
                 setSeriesId(null);
-            } else if (TextUtils.isEmpty(mSeriesId) && !TextUtils.isEmpty(mEpisodeNumber)) {
+            } else if (TextUtils.isEmpty(getSeriesId()) && !TextUtils.isEmpty(getEpisodeNumber())) {
                 // If series ID is not set, generate it for the episodic program of other TV input.
-                setSeriesId(BaseProgram.generateSeriesId(mPackageName, mTitle));
+                setSeriesId(BaseProgram.generateSeriesId(getPackageName(), getTitle()));
             }
-            return new RecordedProgram(
-                    mId,
-                    mPackageName,
-                    mInputId,
-                    mChannelId,
-                    mTitle,
-                    mSeriesId,
-                    mSeasonNumber,
-                    mSeasonTitle,
-                    mEpisodeNumber,
-                    mEpisodeTitle,
-                    mStartTimeUtcMillis,
-                    mEndTimeUtcMillis,
-                    mBroadcastGenres,
-                    mCanonicalGenres,
-                    mShortDescription,
-                    mLongDescription,
-                    mVideoWidth,
-                    mVideoHeight,
-                    mAudioLanguage,
-                    mContentRatings,
-                    mPosterArtUri,
-                    mThumbnailUri,
-                    mSearchable,
-                    mDataUri,
-                    mDataBytes,
-                    mDurationMillis,
-                    mExpireTimeUtcMillis,
-                    mInternalProviderFlag1,
-                    mInternalProviderFlag2,
-                    mInternalProviderFlag3,
-                    mInternalProviderFlag4,
-                    mVersionNumber);
+            return (autoBuild());
         }
     }
 
     public static Builder builder() {
-        return new Builder();
-    }
-
-    public static Builder buildFrom(RecordedProgram orig) {
-        return builder()
-                .setId(orig.getId())
-                .setPackageName(orig.getPackageName())
-                .setInputId(orig.getInputId())
-                .setChannelId(orig.getChannelId())
-                .setTitle(orig.getTitle())
-                .setSeriesId(orig.getSeriesId())
-                .setSeasonNumber(orig.getSeasonNumber())
-                .setSeasonTitle(orig.getSeasonTitle())
-                .setEpisodeNumber(orig.getEpisodeNumber())
-                .setEpisodeTitle(orig.getEpisodeTitle())
-                .setStartTimeUtcMillis(orig.getStartTimeUtcMillis())
-                .setEndTimeUtcMillis(orig.getEndTimeUtcMillis())
-                .setBroadcastGenres(orig.getBroadcastGenres())
-                .setCanonicalGenres(orig.getCanonicalGenres())
-                .setShortDescription(orig.getDescription())
-                .setLongDescription(orig.getLongDescription())
-                .setVideoWidth(orig.getVideoWidth())
-                .setVideoHeight(orig.getVideoHeight())
-                .setAudioLanguage(orig.getAudioLanguage())
-                .setContentRatings(orig.getContentRatings())
-                .setPosterArtUri(orig.getPosterArtUri())
-                .setThumbnailUri(orig.getThumbnailUri())
-                .setSearchable(orig.isSearchable())
-                .setInternalProviderFlag1(orig.getInternalProviderFlag1())
-                .setInternalProviderFlag2(orig.getInternalProviderFlag2())
-                .setInternalProviderFlag3(orig.getInternalProviderFlag3())
-                .setInternalProviderFlag4(orig.getInternalProviderFlag4())
-                .setVersionNumber(orig.getVersionNumber());
+        return new AutoValue_RecordedProgram.Builder()
+                .setId(ID_NOT_SET)
+                .setChannelId(ID_NOT_SET)
+                .setAudioLanguage("")
+                .setBroadcastGenres("")
+                .setCanonicalGenres("")
+                .setContentRatings(ImmutableList.of())
+                .setDataUri("")
+                .setDurationMillis(0)
+                .setDescription("")
+                .setDataBytes(0)
+                .setLongDescription("")
+                .setEndTimeUtcMillis(0)
+                .setEpisodeNumber("")
+                .setEpisodeTitle("")
+                .setExpireTimeUtcMillis(0)
+                .setPackageName("")
+                .setPosterArtUri("")
+                .setSeasonNumber("")
+                .setSeasonTitle("")
+                .setSearchable(false)
+                .setSeriesId("")
+                .setStartTimeUtcMillis(0)
+                .setState(RecordedProgramState.NOT_SET)
+                .setThumbnailUri("")
+                .setTitle("")
+                .setVersionNumber(0)
+                .setVideoHeight(0)
+                .setVideoWidth(0);
     }
 
     public static final Comparator<RecordedProgram> START_TIME_THEN_ID_COMPARATOR =
-            new Comparator<RecordedProgram>() {
-                @Override
-                public int compare(RecordedProgram lhs, RecordedProgram rhs) {
-                    int res =
-                            Long.compare(lhs.getStartTimeUtcMillis(), rhs.getStartTimeUtcMillis());
-                    if (res != 0) {
-                        return res;
-                    }
-                    return Long.compare(lhs.mId, rhs.mId);
+            (RecordedProgram lhs, RecordedProgram rhs) -> {
+                int res = Long.compare(lhs.getStartTimeUtcMillis(), rhs.getStartTimeUtcMillis());
+                if (res != 0) {
+                    return res;
                 }
+                return Long.compare(lhs.getId(), rhs.getId());
             };
 
     private static final long CLIPPED_THRESHOLD_MS = TimeUnit.MINUTES.toMillis(5);
 
-    private final long mId;
-    private final String mPackageName;
-    private final String mInputId;
-    private final long mChannelId;
-    private final String mTitle;
-    private final String mSeriesId;
-    private final String mSeasonNumber;
-    private final String mSeasonTitle;
-    private final String mEpisodeNumber;
-    private final String mEpisodeTitle;
-    private final long mStartTimeUtcMillis;
-    private final long mEndTimeUtcMillis;
-    private final String[] mBroadcastGenres;
-    private final String[] mCanonicalGenres;
-    private final String mShortDescription;
-    private final String mLongDescription;
-    private final int mVideoWidth;
-    private final int mVideoHeight;
-    private final String mAudioLanguage;
-    private final TvContentRating[] mContentRatings;
-    private final String mPosterArtUri;
-    private final String mThumbnailUri;
-    private final boolean mSearchable;
-    private final Uri mDataUri;
-    private final long mDataBytes;
-    private final long mDurationMillis;
-    private final long mExpireTimeUtcMillis;
-    private final int mInternalProviderFlag1;
-    private final int mInternalProviderFlag2;
-    private final int mInternalProviderFlag3;
-    private final int mInternalProviderFlag4;
-    private final int mVersionNumber;
+    public abstract String getAudioLanguage();
 
-    private RecordedProgram(
-            long id,
-            String packageName,
-            String inputId,
-            long channelId,
-            String title,
-            String seriesId,
-            String seasonNumber,
-            String seasonTitle,
-            String episodeNumber,
-            String episodeTitle,
-            long startTimeUtcMillis,
-            long endTimeUtcMillis,
-            String[] broadcastGenres,
-            String[] canonicalGenres,
-            String shortDescription,
-            String longDescription,
-            int videoWidth,
-            int videoHeight,
-            String audioLanguage,
-            TvContentRating[] contentRatings,
-            String posterArtUri,
-            String thumbnailUri,
-            boolean searchable,
-            Uri dataUri,
-            long dataBytes,
-            long durationMillis,
-            long expireTimeUtcMillis,
-            int internalProviderFlag1,
-            int internalProviderFlag2,
-            int internalProviderFlag3,
-            int internalProviderFlag4,
-            int versionNumber) {
-        mId = id;
-        mPackageName = packageName;
-        mInputId = inputId;
-        mChannelId = channelId;
-        mTitle = title;
-        mSeriesId = seriesId;
-        mSeasonNumber = seasonNumber;
-        mSeasonTitle = seasonTitle;
-        mEpisodeNumber = episodeNumber;
-        mEpisodeTitle = episodeTitle;
-        mStartTimeUtcMillis = startTimeUtcMillis;
-        mEndTimeUtcMillis = endTimeUtcMillis;
-        mBroadcastGenres = broadcastGenres;
-        mCanonicalGenres = canonicalGenres;
-        mShortDescription = shortDescription;
-        mLongDescription = longDescription;
-        mVideoWidth = videoWidth;
-        mVideoHeight = videoHeight;
+    public abstract ImmutableList<String> getBroadcastGenres();
 
-        mAudioLanguage = audioLanguage;
-        mContentRatings = contentRatings;
-        mPosterArtUri = posterArtUri;
-        mThumbnailUri = thumbnailUri;
-        mSearchable = searchable;
-        mDataUri = dataUri;
-        mDataBytes = dataBytes;
-        mDurationMillis = durationMillis;
-        mExpireTimeUtcMillis = expireTimeUtcMillis;
-        mInternalProviderFlag1 = internalProviderFlag1;
-        mInternalProviderFlag2 = internalProviderFlag2;
-        mInternalProviderFlag3 = internalProviderFlag3;
-        mInternalProviderFlag4 = internalProviderFlag4;
-        mVersionNumber = versionNumber;
-    }
-
-    public String getAudioLanguage() {
-        return mAudioLanguage;
-    }
-
-    public String[] getBroadcastGenres() {
-        return mBroadcastGenres;
-    }
-
-    public String[] getCanonicalGenres() {
-        return mCanonicalGenres;
-    }
+    public abstract ImmutableList<String> getCanonicalGenres();
 
     /** Returns array of canonical genre ID's for this recorded program. */
     @Override
     public int[] getCanonicalGenreIds() {
-        if (mCanonicalGenres == null) {
-            return null;
-        }
-        int[] genreIds = new int[mCanonicalGenres.length];
-        for (int i = 0; i < mCanonicalGenres.length; i++) {
-            genreIds[i] = GenreItems.getId(mCanonicalGenres[i]);
+
+        ImmutableList<String> canonicalGenres = getCanonicalGenres();
+        int[] genreIds = new int[getCanonicalGenres().size()];
+        for (int i = 0; i < canonicalGenres.size(); i++) {
+            genreIds[i] = GenreItems.getId(canonicalGenres.get(i));
         }
         return genreIds;
     }
 
-    @Override
-    public long getChannelId() {
-        return mChannelId;
-    }
+    public abstract Uri getDataUri();
 
-    @Nullable
-    @Override
-    public TvContentRating[] getContentRatings() {
-        return mContentRatings;
-    }
-
-    public Uri getDataUri() {
-        return mDataUri;
-    }
-
-    public long getDataBytes() {
-        return mDataBytes;
-    }
-
-    @Override
-    public long getDurationMillis() {
-        return mDurationMillis;
-    }
-
-    @Override
-    public long getEndTimeUtcMillis() {
-        return mEndTimeUtcMillis;
-    }
-
-    @Override
-    public String getEpisodeNumber() {
-        return mEpisodeNumber;
-    }
-
-    @Override
-    public String getEpisodeTitle() {
-        return mEpisodeTitle;
-    }
+    public abstract long getDataBytes();
 
     @Nullable
     public String getEpisodeDisplayNumber(Context context) {
-        if (!TextUtils.isEmpty(mEpisodeNumber)) {
-            if (TextUtils.equals(mSeasonNumber, "0")) {
+        if (!TextUtils.isEmpty(getEpisodeNumber())) {
+            if (TextUtils.equals(getSeasonNumber(), "0")) {
                 // Do not show "S0: ".
-                return String.format(
-                        context.getResources()
-                                .getString(R.string.display_episode_number_format_no_season_number),
-                        mEpisodeNumber);
+                return context.getResources()
+                        .getString(
+                                R.string.display_episode_number_format_no_season_number,
+                                getEpisodeNumber());
             } else {
-                return String.format(
-                        context.getResources().getString(R.string.display_episode_number_format),
-                        mSeasonNumber,
-                        mEpisodeNumber);
+                return context.getResources()
+                        .getString(
+                                R.string.display_episode_number_format,
+                                getSeasonNumber(),
+                                getEpisodeNumber());
             }
         }
         return null;
     }
 
-    public long getExpireTimeUtcMillis() {
-        return mExpireTimeUtcMillis;
-    }
+    public abstract long getExpireTimeUtcMillis();
 
-    public long getId() {
-        return mId;
-    }
+    public abstract String getPackageName();
 
-    public String getPackageName() {
-        return mPackageName;
-    }
-
-    public String getInputId() {
-        return mInputId;
-    }
-
-    public int getInternalProviderFlag1() {
-        return mInternalProviderFlag1;
-    }
-
-    public int getInternalProviderFlag2() {
-        return mInternalProviderFlag2;
-    }
-
-    public int getInternalProviderFlag3() {
-        return mInternalProviderFlag3;
-    }
-
-    public int getInternalProviderFlag4() {
-        return mInternalProviderFlag4;
-    }
-
-    @Override
-    public String getDescription() {
-        return mShortDescription;
-    }
-
-    @Override
-    public String getLongDescription() {
-        return mLongDescription;
-    }
-
-    @Override
-    public String getPosterArtUri() {
-        return mPosterArtUri;
-    }
+    public abstract String getInputId();
 
     @Override
     public boolean isValid() {
         return true;
     }
 
-    public boolean isSearchable() {
-        return mSearchable;
+    public boolean isVisible() {
+        switch (getState()) {
+            case NOT_SET:
+            case FINISHED:
+                return true;
+            default:
+                return false;
+        }
     }
 
-    @Override
-    public String getSeriesId() {
-        return mSeriesId;
+    public boolean isPartial() {
+        return getState() == RecordedProgramState.PARTIAL;
     }
 
-    @Override
-    public String getSeasonNumber() {
-        return mSeasonNumber;
-    }
+    public abstract boolean isSearchable();
 
-    public String getSeasonTitle() {
-        return mSeasonTitle;
-    }
+    public abstract String getSeasonTitle();
 
-    @Override
-    public long getStartTimeUtcMillis() {
-        return mStartTimeUtcMillis;
-    }
-
-    @Override
-    public String getThumbnailUri() {
-        return mThumbnailUri;
-    }
-
-    @Override
-    public String getTitle() {
-        return mTitle;
-    }
+    public abstract RecordedProgramState getState();
 
     public Uri getUri() {
-        return ContentUris.withAppendedId(RecordedPrograms.CONTENT_URI, mId);
+        return ContentUris.withAppendedId(RecordedPrograms.CONTENT_URI, getId());
     }
 
-    public int getVersionNumber() {
-        return mVersionNumber;
-    }
+    public abstract int getVersionNumber();
 
-    public int getVideoHeight() {
-        return mVideoHeight;
-    }
+    public abstract int getVideoHeight();
 
-    public int getVideoWidth() {
-        return mVideoWidth;
-    }
+    public abstract int getVideoWidth();
 
     /** Checks whether the recording has been clipped or not. */
     public boolean isClipped() {
-        return mEndTimeUtcMillis - mStartTimeUtcMillis - mDurationMillis > CLIPPED_THRESHOLD_MS;
+        return getEndTimeUtcMillis() - getStartTimeUtcMillis() - getDurationMillis()
+                > CLIPPED_THRESHOLD_MS;
     }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        RecordedProgram that = (RecordedProgram) o;
-        return Objects.equals(mId, that.mId)
-                && Objects.equals(mChannelId, that.mChannelId)
-                && Objects.equals(mSeriesId, that.mSeriesId)
-                && Objects.equals(mSeasonNumber, that.mSeasonNumber)
-                && Objects.equals(mSeasonTitle, that.mSeasonTitle)
-                && Objects.equals(mEpisodeNumber, that.mEpisodeNumber)
-                && Objects.equals(mStartTimeUtcMillis, that.mStartTimeUtcMillis)
-                && Objects.equals(mEndTimeUtcMillis, that.mEndTimeUtcMillis)
-                && Objects.equals(mVideoWidth, that.mVideoWidth)
-                && Objects.equals(mVideoHeight, that.mVideoHeight)
-                && Objects.equals(mSearchable, that.mSearchable)
-                && Objects.equals(mDataBytes, that.mDataBytes)
-                && Objects.equals(mDurationMillis, that.mDurationMillis)
-                && Objects.equals(mExpireTimeUtcMillis, that.mExpireTimeUtcMillis)
-                && Objects.equals(mInternalProviderFlag1, that.mInternalProviderFlag1)
-                && Objects.equals(mInternalProviderFlag2, that.mInternalProviderFlag2)
-                && Objects.equals(mInternalProviderFlag3, that.mInternalProviderFlag3)
-                && Objects.equals(mInternalProviderFlag4, that.mInternalProviderFlag4)
-                && Objects.equals(mVersionNumber, that.mVersionNumber)
-                && Objects.equals(mTitle, that.mTitle)
-                && Objects.equals(mEpisodeTitle, that.mEpisodeTitle)
-                && Arrays.equals(mBroadcastGenres, that.mBroadcastGenres)
-                && Arrays.equals(mCanonicalGenres, that.mCanonicalGenres)
-                && Objects.equals(mShortDescription, that.mShortDescription)
-                && Objects.equals(mLongDescription, that.mLongDescription)
-                && Objects.equals(mAudioLanguage, that.mAudioLanguage)
-                && Arrays.equals(mContentRatings, that.mContentRatings)
-                && Objects.equals(mPosterArtUri, that.mPosterArtUri)
-                && Objects.equals(mThumbnailUri, that.mThumbnailUri);
-    }
+    public abstract Builder toBuilder();
 
-    /** Hashes based on the ID. */
-    @Override
-    public int hashCode() {
-        return Objects.hash(mId);
-    }
-
-    @Override
-    public String toString() {
-        return "RecordedProgram"
-                + "["
-                + mId
-                + "]{ mPackageName="
-                + mPackageName
-                + ", mInputId='"
-                + mInputId
-                + '\''
-                + ", mChannelId='"
-                + mChannelId
-                + '\''
-                + ", mTitle='"
-                + mTitle
-                + '\''
-                + ", mSeriesId='"
-                + mSeriesId
-                + '\''
-                + ", mEpisodeNumber="
-                + mEpisodeNumber
-                + ", mEpisodeTitle='"
-                + mEpisodeTitle
-                + '\''
-                + ", mStartTimeUtcMillis="
-                + mStartTimeUtcMillis
-                + ", mEndTimeUtcMillis="
-                + mEndTimeUtcMillis
-                + ", mBroadcastGenres="
-                + (mBroadcastGenres != null ? Arrays.toString(mBroadcastGenres) : "null")
-                + ", mCanonicalGenres="
-                + (mCanonicalGenres != null ? Arrays.toString(mCanonicalGenres) : "null")
-                + ", mShortDescription='"
-                + mShortDescription
-                + '\''
-                + ", mLongDescription='"
-                + mLongDescription
-                + '\''
-                + ", mVideoHeight="
-                + mVideoHeight
-                + ", mVideoWidth="
-                + mVideoWidth
-                + ", mAudioLanguage='"
-                + mAudioLanguage
-                + '\''
-                + ", mContentRatings='"
-                + TvContentRatingCache.contentRatingsToString(mContentRatings)
-                + '\''
-                + ", mPosterArtUri="
-                + mPosterArtUri
-                + ", mThumbnailUri="
-                + mThumbnailUri
-                + ", mSearchable="
-                + mSearchable
-                + ", mDataUri="
-                + mDataUri
-                + ", mDataBytes="
-                + mDataBytes
-                + ", mDurationMillis="
-                + mDurationMillis
-                + ", mExpireTimeUtcMillis="
-                + mExpireTimeUtcMillis
-                + ", mInternalProviderFlag1="
-                + mInternalProviderFlag1
-                + ", mInternalProviderFlag2="
-                + mInternalProviderFlag2
-                + ", mInternalProviderFlag3="
-                + mInternalProviderFlag3
-                + ", mInternalProviderFlag4="
-                + mInternalProviderFlag4
-                + ", mSeasonNumber="
-                + mSeasonNumber
-                + ", mSeasonTitle="
-                + mSeasonTitle
-                + ", mVersionNumber="
-                + mVersionNumber
-                + '}';
+    @CheckResult
+    public RecordedProgram withId(long id) {
+        return toBuilder().setId(id).build();
     }
 
     @Nullable
@@ -925,8 +466,8 @@
     }
 
     @Nullable
-    private static String safeEncode(@Nullable String[] genres) {
-        return genres == null ? null : TvContract.Programs.Genres.encode(genres);
+    private static String safeEncode(@Nullable ImmutableList<String> genres) {
+        return genres == null ? null : Genres.encode(genres.toArray(new String[0]));
     }
 
     /** Returns an array containing all of the elements in the list. */
diff --git a/src/com/android/tv/dvr/data/ScheduledRecording.java b/src/com/android/tv/dvr/data/ScheduledRecording.java
index 7c2d12d..ba6d3cf 100644
--- a/src/com/android/tv/dvr/data/ScheduledRecording.java
+++ b/src/com/android/tv/dvr/data/ScheduledRecording.java
@@ -56,39 +56,22 @@
 
     /** Compares the start time in ascending order. */
     public static final Comparator<ScheduledRecording> START_TIME_COMPARATOR =
-            new Comparator<ScheduledRecording>() {
-                @Override
-                public int compare(ScheduledRecording lhs, ScheduledRecording rhs) {
-                    return Long.compare(lhs.mStartTimeMs, rhs.mStartTimeMs);
-                }
-            };
+            (ScheduledRecording lhs, ScheduledRecording rhs) ->
+                    Long.compare(lhs.mStartTimeMs, rhs.mStartTimeMs);
 
     /** Compares the end time in ascending order. */
     public static final Comparator<ScheduledRecording> END_TIME_COMPARATOR =
-            new Comparator<ScheduledRecording>() {
-                @Override
-                public int compare(ScheduledRecording lhs, ScheduledRecording rhs) {
-                    return Long.compare(lhs.mEndTimeMs, rhs.mEndTimeMs);
-                }
-            };
+            (ScheduledRecording lhs, ScheduledRecording rhs) ->
+                    Long.compare(lhs.mEndTimeMs, rhs.mEndTimeMs);
 
     /** Compares ID in ascending order. The schedule with the larger ID was created later. */
     public static final Comparator<ScheduledRecording> ID_COMPARATOR =
-            new Comparator<ScheduledRecording>() {
-                @Override
-                public int compare(ScheduledRecording lhs, ScheduledRecording rhs) {
-                    return Long.compare(lhs.mId, rhs.mId);
-                }
-            };
+            (ScheduledRecording lhs, ScheduledRecording rhs) -> Long.compare(lhs.mId, rhs.mId);
 
     /** Compares the priority in ascending order. */
     public static final Comparator<ScheduledRecording> PRIORITY_COMPARATOR =
-            new Comparator<ScheduledRecording>() {
-                @Override
-                public int compare(ScheduledRecording lhs, ScheduledRecording rhs) {
-                    return Long.compare(lhs.mPriority, rhs.mPriority);
-                }
-            };
+            (ScheduledRecording lhs, ScheduledRecording rhs) ->
+                    Long.compare(lhs.mPriority, rhs.mPriority);
 
     /**
      * Compares start time in ascending order and then priority in descending order and then ID in
@@ -359,15 +342,22 @@
     })
     public @interface RecordingFailedReason {}
 
+    // next number for failed reason: 11
     public static final int FAILED_REASON_OTHER = 0;
-    public static final int FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED = 1;
     public static final int FAILED_REASON_NOT_FINISHED = 2;
     public static final int FAILED_REASON_SCHEDULER_STOPPED = 3;
     public static final int FAILED_REASON_INVALID_CHANNEL = 4;
     public static final int FAILED_REASON_MESSAGE_NOT_SENT = 5;
     public static final int FAILED_REASON_CONNECTION_FAILED = 6;
+
+    // for the following reasons, show advice to users
+    // TODO(b/72638597): add failure condition of "weak signal"
+
+    // failed reason is FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED when tuner or external
+    // storage is disconnected
+    public static final int FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED = 1;
+    // failed reason is FAILED_REASON_RESOURCE_BUSY when antenna is disconnected or signal is weak
     public static final int FAILED_REASON_RESOURCE_BUSY = 7;
-    // For the following reasons, show advice to users
     public static final int FAILED_REASON_INPUT_UNAVAILABLE = 8;
     public static final int FAILED_REASON_INPUT_DVR_UNSUPPORTED = 9;
     public static final int FAILED_REASON_INSUFFICIENT_SPACE = 10;
@@ -679,7 +669,8 @@
     }
 
     /** Returns the failed reason of the {@link ScheduledRecording}. */
-    @Nullable @RecordingFailedReason
+    @Nullable
+    @RecordingFailedReason
     public Integer getFailedReason() {
         return mFailedReason;
     }
@@ -812,10 +803,7 @@
         }
     }
 
-    /**
-     * Converts a string to a failed reason integer, defaulting to {@link
-     * #FAILED_REASON_OTHER}.
-     */
+    /** Converts a string to a failed reason integer, defaulting to {@link #FAILED_REASON_OTHER}. */
     private static Integer recordingFailedReason(String reason) {
         if (TextUtils.isEmpty(reason)) {
             return null;
@@ -985,6 +973,11 @@
         return mState == STATE_RECORDING_FINISHED;
     }
 
+    /** Returns {@code true} if the recording is failed, otherwise @{code false}. */
+    public boolean isFailed() {
+        return mState == STATE_RECORDING_FAILED;
+    }
+
     @Override
     public boolean equals(Object obj) {
         if (!(obj instanceof ScheduledRecording)) {
diff --git a/src/com/android/tv/dvr/data/SeriesRecording.java b/src/com/android/tv/dvr/data/SeriesRecording.java
index 96b3425..6cb0e83 100644
--- a/src/com/android/tv/dvr/data/SeriesRecording.java
+++ b/src/com/android/tv/dvr/data/SeriesRecording.java
@@ -49,9 +49,8 @@
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(
-        flag = true,
-        value = {OPTION_CHANNEL_ONE, OPTION_CHANNEL_ALL}
-    )
+            flag = true,
+            value = {OPTION_CHANNEL_ONE, OPTION_CHANNEL_ALL})
     public @interface ChannelOption {}
     /** An option which indicates that the episodes in one channel are recorded. */
     public static final int OPTION_CHANNEL_ONE = 0;
@@ -60,9 +59,8 @@
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(
-        flag = true,
-        value = {STATE_SERIES_NORMAL, STATE_SERIES_STOPPED}
-    )
+            flag = true,
+            value = {STATE_SERIES_NORMAL, STATE_SERIES_STOPPED})
     public @interface SeriesState {}
 
     /** The state indicates that the series recording is a normal one. */
@@ -73,26 +71,18 @@
 
     /** Compare priority in descending order. */
     public static final Comparator<SeriesRecording> PRIORITY_COMPARATOR =
-            new Comparator<SeriesRecording>() {
-                @Override
-                public int compare(SeriesRecording lhs, SeriesRecording rhs) {
-                    int value = Long.compare(rhs.mPriority, lhs.mPriority);
-                    if (value == 0) {
-                        // New recording has the higher priority.
-                        value = Long.compare(rhs.mId, lhs.mId);
-                    }
-                    return value;
+            (SeriesRecording lhs, SeriesRecording rhs) -> {
+                int value = Long.compare(rhs.mPriority, lhs.mPriority);
+                if (value == 0) {
+                    // New recording has the higher priority.
+                    value = Long.compare(rhs.mId, lhs.mId);
                 }
+                return value;
             };
 
     /** Compare ID in ascending order. */
     public static final Comparator<SeriesRecording> ID_COMPARATOR =
-            new Comparator<SeriesRecording>() {
-                @Override
-                public int compare(SeriesRecording lhs, SeriesRecording rhs) {
-                    return Long.compare(lhs.mId, rhs.mId);
-                }
-            };
+            (SeriesRecording lhs, SeriesRecording rhs) -> Long.compare(lhs.mId, rhs.mId);
 
     /**
      * Creates a new Builder with the values set from the series information of {@link BaseProgram}.
diff --git a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java
deleted file mode 100644
index 7d2af9c..0000000
--- a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tv.dvr.provider;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.os.AsyncTask;
-import android.support.annotation.Nullable;
-import com.android.tv.common.concurrent.NamedThreadFactory;
-import com.android.tv.dvr.data.ScheduledRecording;
-import com.android.tv.dvr.data.SeriesRecording;
-import com.android.tv.dvr.provider.DvrContract.Schedules;
-import com.android.tv.dvr.provider.DvrContract.SeriesRecordings;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-/** {@link AsyncTask} that defaults to executing on its own single threaded Executor Service. */
-public abstract class AsyncDvrDbTask<Params, Progress, Result>
-        extends AsyncTask<Params, Progress, Result> {
-    private static final NamedThreadFactory THREAD_FACTORY =
-            new NamedThreadFactory(AsyncDvrDbTask.class.getSimpleName());
-    private static final ExecutorService DB_EXECUTOR =
-            Executors.newSingleThreadExecutor(THREAD_FACTORY);
-
-    private static DvrDatabaseHelper sDbHelper;
-
-    private static synchronized DvrDatabaseHelper initializeDbHelper(Context context) {
-        if (sDbHelper == null) {
-            sDbHelper = new DvrDatabaseHelper(context.getApplicationContext());
-        }
-        return sDbHelper;
-    }
-
-    final Context mContext;
-
-    private AsyncDvrDbTask(Context context) {
-        mContext = context;
-    }
-
-    /** Execute the task on the {@link #DB_EXECUTOR} thread. */
-    @SafeVarargs
-    public final void executeOnDbThread(Params... params) {
-        executeOnExecutor(DB_EXECUTOR, params);
-    }
-
-    @Override
-    protected final Result doInBackground(Params... params) {
-        initializeDbHelper(mContext);
-        return doInDvrBackground(params);
-    }
-
-    /** Executes in the background after {@link #initializeDbHelper(Context)} */
-    @Nullable
-    protected abstract Result doInDvrBackground(Params... params);
-
-    /** Inserts schedules. */
-    public static class AsyncAddScheduleTask
-            extends AsyncDvrDbTask<ScheduledRecording, Void, Void> {
-        public AsyncAddScheduleTask(Context context) {
-            super(context);
-        }
-
-        @Override
-        protected final Void doInDvrBackground(ScheduledRecording... params) {
-            sDbHelper.insertSchedules(params);
-            return null;
-        }
-    }
-
-    /** Update schedules. */
-    public static class AsyncUpdateScheduleTask
-            extends AsyncDvrDbTask<ScheduledRecording, Void, Void> {
-        public AsyncUpdateScheduleTask(Context context) {
-            super(context);
-        }
-
-        @Override
-        protected final Void doInDvrBackground(ScheduledRecording... params) {
-            sDbHelper.updateSchedules(params);
-            return null;
-        }
-    }
-
-    /** Delete schedules. */
-    public static class AsyncDeleteScheduleTask
-            extends AsyncDvrDbTask<ScheduledRecording, Void, Void> {
-        public AsyncDeleteScheduleTask(Context context) {
-            super(context);
-        }
-
-        @Override
-        protected final Void doInDvrBackground(ScheduledRecording... params) {
-            sDbHelper.deleteSchedules(params);
-            return null;
-        }
-    }
-
-    /** Returns all {@link ScheduledRecording}s. */
-    public abstract static class AsyncDvrQueryScheduleTask
-            extends AsyncDvrDbTask<Void, Void, List<ScheduledRecording>> {
-        public AsyncDvrQueryScheduleTask(Context context) {
-            super(context);
-        }
-
-        @Override
-        @Nullable
-        protected final List<ScheduledRecording> doInDvrBackground(Void... params) {
-            if (isCancelled()) {
-                return null;
-            }
-            List<ScheduledRecording> scheduledRecordings = new ArrayList<>();
-            try (Cursor c = sDbHelper.query(Schedules.TABLE_NAME, ScheduledRecording.PROJECTION)) {
-                while (c.moveToNext() && !isCancelled()) {
-                    scheduledRecordings.add(ScheduledRecording.fromCursor(c));
-                }
-            }
-            return scheduledRecordings;
-        }
-    }
-
-    /** Inserts series recordings. */
-    public static class AsyncAddSeriesRecordingTask
-            extends AsyncDvrDbTask<SeriesRecording, Void, Void> {
-        public AsyncAddSeriesRecordingTask(Context context) {
-            super(context);
-        }
-
-        @Override
-        protected final Void doInDvrBackground(SeriesRecording... params) {
-            sDbHelper.insertSeriesRecordings(params);
-            return null;
-        }
-    }
-
-    /** Update series recordings. */
-    public static class AsyncUpdateSeriesRecordingTask
-            extends AsyncDvrDbTask<SeriesRecording, Void, Void> {
-        public AsyncUpdateSeriesRecordingTask(Context context) {
-            super(context);
-        }
-
-        @Override
-        protected final Void doInDvrBackground(SeriesRecording... params) {
-            sDbHelper.updateSeriesRecordings(params);
-            return null;
-        }
-    }
-
-    /** Delete series recordings. */
-    public static class AsyncDeleteSeriesRecordingTask
-            extends AsyncDvrDbTask<SeriesRecording, Void, Void> {
-        public AsyncDeleteSeriesRecordingTask(Context context) {
-            super(context);
-        }
-
-        @Override
-        protected final Void doInDvrBackground(SeriesRecording... params) {
-            sDbHelper.deleteSeriesRecordings(params);
-            return null;
-        }
-    }
-
-    /** Returns all {@link SeriesRecording}s. */
-    public abstract static class AsyncDvrQuerySeriesRecordingTask
-            extends AsyncDvrDbTask<Void, Void, List<SeriesRecording>> {
-        public AsyncDvrQuerySeriesRecordingTask(Context context) {
-            super(context);
-        }
-
-        @Override
-        @Nullable
-        protected final List<SeriesRecording> doInDvrBackground(Void... params) {
-            if (isCancelled()) {
-                return null;
-            }
-            List<SeriesRecording> scheduledRecordings = new ArrayList<>();
-            try (Cursor c =
-                    sDbHelper.query(SeriesRecordings.TABLE_NAME, SeriesRecording.PROJECTION)) {
-                while (c.moveToNext() && !isCancelled()) {
-                    scheduledRecordings.add(SeriesRecording.fromCursor(c));
-                }
-            }
-            return scheduledRecordings;
-        }
-    }
-}
diff --git a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
index 41e5a66..ebf133d 100644
--- a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
+++ b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
@@ -79,6 +79,8 @@
                     + " TEXT,"
                     + Schedules.COLUMN_STATE
                     + " TEXT NOT NULL,"
+                    + Schedules.COLUMN_FAILED_REASON
+                    + " TEXT,"
                     + Schedules.COLUMN_SERIES_RECORDING_ID
                     + " INTEGER,"
                     + "FOREIGN KEY("
@@ -261,6 +263,7 @@
             if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_SERIES_RECORDINGS);
             db.execSQL(SQL_DROP_SERIES_RECORDINGS);
             onCreate(db);
+            return;
         }
         if (oldVersion < 18) {
             db.execSQL("ALTER TABLE " + Schedules.TABLE_NAME + " ADD COLUMN "
diff --git a/src/com/android/tv/dvr/provider/DvrDbFuture.java b/src/com/android/tv/dvr/provider/DvrDbFuture.java
new file mode 100644
index 0000000..ae8c480
--- /dev/null
+++ b/src/com/android/tv/dvr/provider/DvrDbFuture.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.provider;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.support.annotation.Nullable;
+import android.util.Log;
+import com.android.tv.common.concurrent.NamedThreadFactory;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.provider.DvrContract.Schedules;
+import com.android.tv.dvr.provider.DvrContract.SeriesRecordings;
+import com.android.tv.util.MainThreadExecutor;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+
+/** {@link DvrDbFuture} that defaults to executing on its own single threaded Executor Service. */
+public abstract class DvrDbFuture<ParamsT, ResultT> {
+    private static final NamedThreadFactory THREAD_FACTORY =
+        new NamedThreadFactory(DvrDbFuture.class.getSimpleName());
+    private static final ListeningExecutorService DB_EXECUTOR =
+        MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(THREAD_FACTORY));
+
+    private static DvrDatabaseHelper sDbHelper;
+    private ListenableFuture<ResultT> mFuture;
+
+    final Context mContext;
+
+    private DvrDbFuture(Context context) {
+        mContext = context;
+    }
+
+    /** Execute the task on the {@link #DB_EXECUTOR} thread and return Future*/
+    @SafeVarargs
+    public final ListenableFuture<ResultT> executeOnDbThread(
+        FutureCallback<ResultT> callback, ParamsT... params) {
+            if (sDbHelper == null) {
+                sDbHelper = new DvrDatabaseHelper(mContext.getApplicationContext());
+            }
+            mFuture = DB_EXECUTOR.submit(() -> dbHelperInBackground(params));
+            Futures.addCallback(mFuture, callback, MainThreadExecutor.getInstance());
+            return mFuture;
+    }
+
+    /** Executes in the background after initializing DbHelper} */
+    @Nullable
+    protected abstract ResultT dbHelperInBackground(ParamsT... params);
+
+    public final boolean isCancelled() {
+        return mFuture.isCancelled();
+    }
+
+    /** Inserts schedules. */
+    public static class AddScheduleFuture
+            extends DvrDbFuture<ScheduledRecording, Void> {
+        public AddScheduleFuture(Context context) {
+            super(context);
+        }
+
+        @Override
+        protected final Void dbHelperInBackground(ScheduledRecording... params) {
+            sDbHelper.insertSchedules(params);
+            return null;
+        }
+    }
+
+    /** Update schedules. */
+    public static class UpdateScheduleFuture
+            extends DvrDbFuture<ScheduledRecording, Void> {
+        public UpdateScheduleFuture(Context context) {
+            super(context);
+        }
+
+        @Override
+        protected final Void dbHelperInBackground(ScheduledRecording... params) {
+            sDbHelper.updateSchedules(params);
+            return null;
+        }
+    }
+
+    /** Delete schedules. */
+    public static class DeleteScheduleFuture
+            extends DvrDbFuture<ScheduledRecording, Void> {
+        public DeleteScheduleFuture(Context context) {
+            super(context);
+        }
+
+        @Override
+        protected final Void dbHelperInBackground(ScheduledRecording... params) {
+            sDbHelper.deleteSchedules(params);
+            return null;
+        }
+    }
+
+    /** Returns all {@link ScheduledRecording}s. */
+    public static class DvrQueryScheduleFuture
+            extends DvrDbFuture<Void, List<ScheduledRecording>> {
+        public DvrQueryScheduleFuture(Context context) {
+            super(context);
+        }
+
+        @Override
+        @Nullable
+        protected final List<ScheduledRecording> dbHelperInBackground(Void... params) {
+            if (isCancelled()) {
+                return null;
+            }
+            List<ScheduledRecording> scheduledRecordings = new ArrayList<>();
+            try (Cursor c = sDbHelper.query(Schedules.TABLE_NAME, ScheduledRecording.PROJECTION)) {
+                while (c.moveToNext() && !isCancelled()) {
+                    scheduledRecordings.add(ScheduledRecording.fromCursor(c));
+                }
+            }
+            return scheduledRecordings;
+        }
+    }
+
+    /** Inserts series recordings. */
+    public static class AddSeriesRecordingFuture
+            extends DvrDbFuture<SeriesRecording, Void> {
+        public AddSeriesRecordingFuture(Context context) {
+            super(context);
+        }
+
+        @Override
+        protected final Void dbHelperInBackground(SeriesRecording... params) {
+            sDbHelper.insertSeriesRecordings(params);
+            return null;
+        }
+    }
+
+    /** Update series recordings. */
+    public static class UpdateSeriesRecordingFuture
+            extends DvrDbFuture<SeriesRecording, Void> {
+        public UpdateSeriesRecordingFuture(Context context) {
+            super(context);
+        }
+
+        @Override
+        protected final Void dbHelperInBackground(SeriesRecording... params) {
+            sDbHelper.updateSeriesRecordings(params);
+            return null;
+        }
+    }
+
+    /** Delete series recordings. */
+    public static class DeleteSeriesRecordingFuture
+            extends DvrDbFuture<SeriesRecording, Void> {
+        public DeleteSeriesRecordingFuture(Context context) {
+            super(context);
+        }
+
+        @Override
+        protected final Void dbHelperInBackground(SeriesRecording... params) {
+            sDbHelper.deleteSeriesRecordings(params);
+            return null;
+        }
+    }
+
+    /** Returns all {@link SeriesRecording}s. */
+    public static class DvrQuerySeriesRecordingFuture
+            extends DvrDbFuture<Void, List<SeriesRecording>> {
+        private static final String TAG = "DvrQuerySeriesRecording";
+
+        public DvrQuerySeriesRecordingFuture(Context context) {
+            super(context);
+        }
+
+        @Override
+        @Nullable
+        protected final List<SeriesRecording> dbHelperInBackground(Void... params) {
+            if (isCancelled()) {
+                return null;
+            }
+            List<SeriesRecording> scheduledRecordings = new ArrayList<>();
+            try (Cursor c =
+                    sDbHelper.query(SeriesRecordings.TABLE_NAME, SeriesRecording.PROJECTION)) {
+                while (c.moveToNext() && !isCancelled()) {
+                    scheduledRecordings.add(SeriesRecording.fromCursor(c));
+                }
+            } catch (Exception e) {
+                Log.w(TAG, "Can't query dvr series recording data", e);
+            }
+            return scheduledRecordings;
+        }
+    }
+}
diff --git a/src/com/android/tv/dvr/provider/DvrDbSync.java b/src/com/android/tv/dvr/provider/DvrDbSync.java
index 42bc8bc..7658ca4 100644
--- a/src/com/android/tv/dvr/provider/DvrDbSync.java
+++ b/src/com/android/tv/dvr/provider/DvrDbSync.java
@@ -277,7 +277,6 @@
                     }
                 }
             } else {
-                long currentTimeMs = System.currentTimeMillis();
                 ScheduledRecording.Builder builder =
                         ScheduledRecording.buildFrom(schedule)
                                 .setEndTimeMs(program.getEndTimeUtcMillis())
@@ -361,7 +360,7 @@
         private final long mProgramId;
 
         QueryProgramTask(long programId) {
-            super(mDbExecutor, mContext.getContentResolver(), programId);
+            super(mDbExecutor, mContext, programId);
             mProgramId = programId;
         }
 
diff --git a/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java b/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java
index b7d9f3b..02e197f 100644
--- a/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java
+++ b/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java
@@ -186,7 +186,7 @@
         SqlParams sqlParams = createSqlParams();
         return new AsyncProgramQueryTask(
                 TvSingletons.getSingletons(mContext).getDbExecutor(),
-                mContext.getContentResolver(),
+                mContext,
                 sqlParams.uri,
                 sqlParams.selection,
                 sqlParams.selectionArgs,
@@ -284,7 +284,7 @@
 
         @Override
         @WorkerThread
-        public boolean filter(Cursor c) {
+        public boolean apply(Cursor c) {
             if (!mLoadDisallowedProgram
                     && mDisallowedProgramIds.contains(c.getLong(PROGRAM_ID_INDEX))) {
                 return false;
@@ -318,10 +318,10 @@
         }
 
         @Override
-        public boolean filter(Cursor c) {
+        public boolean apply(Cursor c) {
             return (mLoadCurrentProgram || c.getLong(START_TIME_INDEX) > System.currentTimeMillis())
                     && c.getInt(RECORDING_PROHIBITED_INDEX) != 0
-                    && super.filter(c);
+                    && super.apply(c);
         }
     }
 
diff --git a/src/com/android/tv/dvr/recorder/InputTaskScheduler.java b/src/com/android/tv/dvr/recorder/InputTaskScheduler.java
index 1021b2b..7d9f7fe 100644
--- a/src/com/android/tv/dvr/recorder/InputTaskScheduler.java
+++ b/src/com/android/tv/dvr/recorder/InputTaskScheduler.java
@@ -279,7 +279,8 @@
             if (schedule.getEndTimeMs() - currentTimeMs
                     <= MIN_REMAIN_DURATION_PERCENT * schedule.getDuration()) {
                 Log.e(TAG, "Error! Program ended before recording started:" + schedule);
-                fail(schedule,
+                fail(
+                        schedule,
                         ScheduledRecording.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED);
                 iter.remove();
             }
@@ -394,19 +395,16 @@
     private void fail(ScheduledRecording schedule, int reason) {
         // It's called when the scheduling has been failed without creating RecordingTask.
         runOnMainHandler(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        ScheduledRecording scheduleInManager =
-                                mDataManager.getScheduledRecording(schedule.getId());
-                        if (scheduleInManager != null) {
-                            // The schedule should be updated based on the object from DataManager
-                            // in case when it has been updated.
-                            mDataManager.changeState(
-                                    scheduleInManager,
-                                    ScheduledRecording.STATE_RECORDING_FAILED,
-                                    reason);
-                        }
+                () -> {
+                    ScheduledRecording scheduleInManager =
+                            mDataManager.getScheduledRecording(schedule.getId());
+                    if (scheduleInManager != null) {
+                        // The schedule should be updated based on the object from DataManager
+                        // in case when it has been updated.
+                        mDataManager.changeState(
+                                scheduleInManager,
+                                ScheduledRecording.STATE_RECORDING_FAILED,
+                                reason);
                     }
                 });
     }
diff --git a/src/com/android/tv/dvr/recorder/RecordingTask.java b/src/com/android/tv/dvr/recorder/RecordingTask.java
index 07a29e5..98f668a 100644
--- a/src/com/android/tv/dvr/recorder/RecordingTask.java
+++ b/src/com/android/tv/dvr/recorder/RecordingTask.java
@@ -17,10 +17,11 @@
 package com.android.tv.dvr.recorder;
 
 import android.annotation.TargetApi;
+import android.content.ContentUris;
+import android.content.ContentValues;
 import android.content.Context;
 import android.media.tv.TvContract;
 import android.media.tv.TvInputManager;
-import android.media.tv.TvRecordingClient.RecordingCallback;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Handler;
@@ -36,6 +37,7 @@
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
 import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.compat.TvRecordingClientCompat.RecordingCallbackCompat;
 import com.android.tv.common.util.Clock;
 import com.android.tv.common.util.CommonUtils;
 import com.android.tv.data.api.Channel;
@@ -55,7 +57,7 @@
  */
 @WorkerThread
 @TargetApi(Build.VERSION_CODES.N)
-public class RecordingTask extends RecordingCallback
+public class RecordingTask extends RecordingCallbackCompat
         implements Handler.Callback, DvrManager.Listener {
     private static final String TAG = "RecordingTask";
     private static final boolean DEBUG = false;
@@ -223,6 +225,14 @@
     }
 
     @Override
+    public void onRecordingStarted(String inputId, String recUri) {
+        if (DEBUG) {
+            Log.d(TAG, "onRecordingStart");
+        }
+        addRecordedProgramId(recUri);
+    }
+
+    @Override
     public void onRecordingStopped(Uri recordedProgramUri) {
         Log.i(TAG, "Recording Stopped: " + mScheduledRecording);
         Log.i(TAG, "Recording Stopped: stored as " + recordedProgramUri);
@@ -340,10 +350,8 @@
     }
 
     private void failAndQuit(Integer reason) {
-        if (DEBUG) Log.d(TAG, "failAndQuit");
-        updateRecordingState(
-                ScheduledRecording.STATE_RECORDING_FAILED,
-                reason);
+        Log.w(TAG, "Recording " + mScheduledRecording + " failed with code " + reason);
+        updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED, reason);
         mState = State.ERROR;
         sendRemove();
     }
@@ -450,6 +458,7 @@
     private void updateRecordingState(@ScheduledRecording.RecordingState int state) {
         updateRecordingState(state, null);
     }
+
     private void updateRecordingState(
             @ScheduledRecording.RecordingState int state, @Nullable Integer reason) {
         if (DEBUG) {
@@ -471,9 +480,7 @@
                             // has been updated. mScheduledRecording will be updated from
                             // onScheduledRecordingStateChanged.
                             ScheduledRecording.Builder builder =
-                                    ScheduledRecording
-                                            .buildFrom(schedule)
-                                            .setState(state);
+                                    ScheduledRecording.buildFrom(schedule).setState(state);
                             if (state == ScheduledRecording.STATE_RECORDING_FAILED
                                     && reason != null) {
                                 builder.setFailedReason(reason);
@@ -484,6 +491,43 @@
                 });
     }
 
+    private void addRecordedProgramId(String recordedProgramUri) {
+        if (DEBUG) {
+            Log.d(TAG, "Adding Recorded Program Id to " + mScheduledRecording);
+        }
+        mRecordedProgramUri = Uri.parse(recordedProgramUri);
+        long id = ContentUris.parseId(mRecordedProgramUri);
+        mScheduledRecording =
+                ScheduledRecording.buildFrom(mScheduledRecording).setRecordedProgramId(id).build();
+        ContentValues values = new ContentValues();
+        values.put(
+                TvContract.RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS,
+                mScheduledRecording.getEndTimeMs() - mScheduledRecording.getStartTimeMs());
+        values.put(
+                TvContract.RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS,
+                mScheduledRecording.getEndTimeMs());
+        mContext.getContentResolver().update(mRecordedProgramUri, values, null, null);
+        runOnMainThread(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        ScheduledRecording schedule =
+                                mDataManager.getScheduledRecording(mScheduledRecording.getId());
+                        if (schedule == null) {
+                            // Schedule has been deleted. Delete the recorded program.
+                            removeRecordedProgram();
+                        } else {
+                            // Update the state based on the object in DataManager in case when it
+                            // has been updated. mScheduledRecording will be updated from
+                            // onScheduledRecordingStateChanged.
+                            ScheduledRecording.Builder builder =
+                                    ScheduledRecording.buildFrom(schedule).setRecordedProgramId(id);
+                            mDataManager.updateScheduledRecording(builder.build());
+                        }
+                    }
+                });
+    }
+
     @Override
     public void onStopRecordingRequested(ScheduledRecording recording) {
         if (recording.getId() != mScheduledRecording.getId()) {
@@ -553,7 +597,7 @@
                     @Override
                     public void run() {
                         if (mRecordedProgramUri != null) {
-                            mDvrManager.removeRecordedProgram(mRecordedProgramUri);
+                            mDvrManager.removeRecordedProgram(mRecordedProgramUri, true);
                         }
                     }
                 });
diff --git a/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java b/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java
index 4f7a789..696038c 100644
--- a/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java
+++ b/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java
@@ -29,7 +29,6 @@
 import android.util.LongSparseArray;
 import com.android.tv.TvSingletons;
 import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.experiments.Experiments;
 import com.android.tv.common.util.CollectionUtils;
 import com.android.tv.common.util.SharedPreferencesUtils;
 import com.android.tv.data.Program;
@@ -48,7 +47,6 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -261,14 +259,11 @@
     }
 
     private void executeFetchSeriesInfoTask(SeriesRecording seriesRecording) {
-        if (Experiments.CLOUD_EPG.get()) {
-            FetchSeriesInfoTask task =
-                    new FetchSeriesInfoTask(
-                            seriesRecording,
-                            TvSingletons.getSingletons(mContext).providesEpgReader());
-            task.execute();
-            mFetchSeriesInfoTasks.put(seriesRecording.getId(), task);
-        }
+        FetchSeriesInfoTask task =
+                new FetchSeriesInfoTask(
+                        seriesRecording, TvSingletons.getSingletons(mContext).providesEpgReader());
+        task.execute();
+        mFetchSeriesInfoTasks.put(seriesRecording.getId(), task);
     }
 
     /** Pauses the updates of the series recordings. */
@@ -442,21 +437,18 @@
             List<Program> programsForEpisode = entry.getValue();
             Collections.sort(
                     programsForEpisode,
-                    new Comparator<Program>() {
-                        @Override
-                        public int compare(Program lhs, Program rhs) {
-                            // Place the existing schedule first.
-                            boolean lhsScheduled = isProgramScheduled(dataManager, lhs);
-                            boolean rhsScheduled = isProgramScheduled(dataManager, rhs);
-                            if (lhsScheduled && !rhsScheduled) {
-                                return -1;
-                            }
-                            if (!lhsScheduled && rhsScheduled) {
-                                return 1;
-                            }
-                            // Sort by the start time in ascending order.
-                            return lhs.compareTo(rhs);
+                    (Program lhs, Program rhs) -> {
+                        // Place the existing schedule first.
+                        boolean lhsScheduled = isProgramScheduled(dataManager, lhs);
+                        boolean rhsScheduled = isProgramScheduled(dataManager, rhs);
+                        if (lhsScheduled && !rhsScheduled) {
+                            return -1;
                         }
+                        if (!lhsScheduled && rhsScheduled) {
+                            return 1;
+                        }
+                        // Sort by the start time in ascending order.
+                        return lhs.compareTo(rhs);
                     });
             boolean added = false;
             // Add all the scheduled programs
diff --git a/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java b/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java
index 3267942..9cd91a6 100644
--- a/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java
+++ b/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java
@@ -27,13 +27,15 @@
 import android.widget.ImageView;
 import android.widget.ImageView.ScaleType;
 import com.android.tv.R;
+import com.android.tv.ui.DetailsActivity;
+
 import java.util.Map;
 
 /**
  * TODO: Remove this class once b/32405620 is fixed. This class is for the workaround of b/32405620
  * and only for the shared element transition between {@link
  * com.android.tv.dvr.ui.browse.RecordingCardView} and {@link
- * com.android.tv.dvr.ui.browse.DvrDetailsActivity}.
+ * DetailsActivity}.
  */
 public class ChangeImageTransformWithScaledParent extends ChangeImageTransform {
     private static final String PROPNAME_MATRIX = "android:changeImageTransform:matrix";
diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
index fce9423..5e3caa9 100644
--- a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
@@ -71,7 +71,7 @@
     public Guidance onCreateGuidance(Bundle savedInstanceState) {
         String title = getString(R.string.dvr_already_recorded_dialog_title);
         String description = getString(R.string.dvr_already_recorded_dialog_description);
-        Drawable image = getResources().getDrawable(R.drawable.ic_warning_white_96dp, null);
+        Drawable image = getResources().getDrawable(R.drawable.quantum_ic_warning_white_96, null);
         return new Guidance(title, description, null, image);
     }
 
diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java
index 456ad83..a6bbe13 100644
--- a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java
@@ -78,7 +78,7 @@
                                 getContext(),
                                 mDuplicate.getStartTimeMs(),
                                 DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE));
-        Drawable image = getResources().getDrawable(R.drawable.ic_warning_white_96dp, null);
+        Drawable image = getResources().getDrawable(R.drawable.quantum_ic_warning_white_96, null);
         return new Guidance(title, description, null, image);
     }
 
diff --git a/src/com/android/tv/dvr/ui/DvrConflictFragment.java b/src/com/android/tv/dvr/ui/DvrConflictFragment.java
index 6575955..649cc89 100644
--- a/src/com/android/tv/dvr/ui/DvrConflictFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrConflictFragment.java
@@ -205,7 +205,7 @@
             if (description == null) {
                 dismissDialog();
             }
-            Drawable icon = getResources().getDrawable(R.drawable.ic_error_white_48dp, null);
+            Drawable icon = getResources().getDrawable(R.drawable.quantum_ic_error_white_48, null);
             return new Guidance(title, descriptionPrefix + " " + description, null, icon);
         }
 
@@ -265,7 +265,7 @@
             if (description == null) {
                 dismissDialog();
             }
-            Drawable icon = getResources().getDrawable(R.drawable.ic_error_white_48dp, null);
+            Drawable icon = getResources().getDrawable(R.drawable.quantum_ic_error_white_48, null);
             return new Guidance(title, descriptionPrefix + " " + description, null, icon);
         }
 
diff --git a/src/com/android/tv/dvr/ui/DvrFutureProgramInfoFragment.java b/src/com/android/tv/dvr/ui/DvrFutureProgramInfoFragment.java
deleted file mode 100644
index 677a6cb..0000000
--- a/src/com/android/tv/dvr/ui/DvrFutureProgramInfoFragment.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tv.dvr.ui;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.support.v17.leanback.widget.GuidanceStylist;
-import android.support.v17.leanback.widget.GuidedAction;
-import com.android.tv.TvSingletons;
-import com.android.tv.data.Program;
-import com.android.tv.dvr.data.ScheduledRecording;
-import com.android.tv.util.Utils;
-import java.util.List;
-
-/**
- * A fragment which shows the formation of a program.
- */
-public class DvrFutureProgramInfoFragment extends DvrGuidedStepFragment {
-    private static final long ACTION_ID_VIEW_SCHEDULE = 1;
-    private ScheduledRecording mScheduledRecording;
-    private Program mProgram;
-
-    @Override
-    public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) {
-        long startTime = mProgram.getStartTimeUtcMillis();
-        // TODO(b/71717923): use R.string when the strings are finalized
-        StringBuilder description = new StringBuilder()
-                .append("This program will start at ")
-                .append(Utils.getDurationString(getContext(), startTime, startTime, false));
-        if (mScheduledRecording != null) {
-            description.append("\nThis program has been scheduled for recording.");
-        }
-        return new GuidanceStylist.Guidance(
-                mProgram.getTitle(), description.toString(), null, null);
-    }
-
-    @Override
-    public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
-        Activity activity = getActivity();
-        mProgram = getArguments().getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM);
-        mScheduledRecording =
-                TvSingletons.getSingletons(getContext())
-                        .getDvrDataManager()
-                        .getScheduledRecordingForProgramId(mProgram.getId());
-        actions.add(
-                new GuidedAction.Builder(activity)
-                        .id(GuidedAction.ACTION_ID_OK)
-                        .title(android.R.string.ok)
-                        .build());
-        if (mScheduledRecording != null) {
-            actions.add(
-                    new GuidedAction.Builder(activity)
-                            .id(ACTION_ID_VIEW_SCHEDULE)
-                            .title("View schedules")
-                            .build());
-        }
-
-    }
-
-    @Override
-    public void onTrackedGuidedActionClicked(GuidedAction action) {
-        if (action.getId() == ACTION_ID_VIEW_SCHEDULE) {
-            DvrUiHelper.startSchedulesActivity(getContext(), mScheduledRecording);
-            return;
-        }
-        dismissDialog();
-    }
-
-    @Override
-    public String getTrackerPrefix() {
-        return "DvrFutureProgramInfoFragment";
-    }
-}
diff --git a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java
index 4a71370..e6b54f6 100644
--- a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java
@@ -18,17 +18,20 @@
 
 import android.app.Activity;
 import android.content.Context;
+import android.content.DialogInterface;
 import android.os.Bundle;
 import android.support.v17.leanback.app.GuidedStepFragment;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+
 import com.android.tv.MainActivity;
 import com.android.tv.R;
 import com.android.tv.dialog.HalfSizedDialogFragment;
 import com.android.tv.dvr.ui.DvrConflictFragment.DvrChannelWatchConflictFragment;
 import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment;
 import com.android.tv.guide.ProgramGuide;
+import com.android.tv.ui.DetailsActivity;
 
 public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment {
     /** Key for input ID. Type: String. */
@@ -187,11 +190,27 @@
         }
     }
 
-    /** A dialog fragment for {@link DvrFutureProgramInfoFragment}. */
-    public static class DvrFutureProgramInfoDialogFragment extends DvrGuidedStepDialogFragment {
+    /** A dialog fragment for {@link DvrWriteStoragePermissionRationaleFragment}. */
+    public static class DvrWriteStoragePermissionRationaleDialogFragment
+            extends DvrGuidedStepDialogFragment {
         @Override
-        protected DvrGuidedStepFragment onCreateGuidedStepFragment() {
-            return new DvrFutureProgramInfoFragment();
+        protected DvrWriteStoragePermissionRationaleFragment onCreateGuidedStepFragment() {
+            return new DvrWriteStoragePermissionRationaleFragment();
+        }
+
+        @Override
+        public void onDismiss(DialogInterface dialog) {
+            Activity activity = getActivity();
+            if (activity instanceof DetailsActivity) {
+                activity.requestPermissions(
+                        new String[] {"android.permission.WRITE_EXTERNAL_STORAGE"},
+                        DetailsActivity.REQUEST_DELETE);
+            } else if (activity instanceof DvrSeriesDeletionActivity) {
+                activity.requestPermissions(
+                        new String[] {"android.permission.WRITE_EXTERNAL_STORAGE"},
+                        DvrSeriesDeletionActivity.REQUEST_DELETE);
+            }
+            super.onDismiss(dialog);
         }
     }
 }
diff --git a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java
index e5f4026..02b2da1 100644
--- a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java
@@ -25,7 +25,7 @@
 import android.support.v17.leanback.widget.GuidedAction;
 import android.util.Log;
 import com.android.tv.R;
-import com.android.tv.dvr.ui.browse.DvrDetailsActivity;
+import com.android.tv.ui.DetailsActivity;
 import java.util.List;
 
 public class DvrMissingStorageErrorFragment extends DvrGuidedStepFragment {
@@ -65,7 +65,7 @@
     @Override
     public void onTrackedGuidedActionClicked(GuidedAction action) {
         Activity activity = getActivity();
-        if (activity instanceof DvrDetailsActivity) {
+        if (activity instanceof DetailsActivity) {
             activity.finish();
         } else {
             dismissDialog();
diff --git a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
index 5251e14..72603d0 100644
--- a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
@@ -34,7 +34,6 @@
 import com.android.tv.dvr.data.ScheduledRecording;
 import com.android.tv.dvr.data.SeriesRecording;
 import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment;
-import com.android.tv.util.Utils;
 import java.util.Collections;
 import java.util.List;
 
@@ -104,12 +103,7 @@
                                     mProgram.getEndTimeUtcMillis(),
                                     DateUtils.FORMAT_SHOW_TIME));
         } else {
-            description =
-                    Utils.getDurationString(
-                            context,
-                            mProgram.getStartTimeUtcMillis(),
-                            mProgram.getEndTimeUtcMillis(),
-                            true);
+            description = mProgram.getDurationString(context);
         }
         actions.add(
                 new GuidedAction.Builder(context)
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
index a2ae1f9..a237f1d 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
@@ -17,16 +17,34 @@
 package com.android.tv.dvr.ui;
 
 import android.app.Activity;
+import android.content.pm.PackageManager;
 import android.os.Bundle;
+import android.support.annotation.NonNull;
 import android.support.v17.leanback.app.GuidedStepFragment;
+import android.util.Log;
+import android.widget.Toast;
+
 import com.android.tv.R;
 import com.android.tv.Starter;
+import com.android.tv.TvSingletons;
+import com.android.tv.dvr.DvrManager;
+
+import java.util.ArrayList;
+import java.util.List;
 
 /** Activity to show details view in DVR. */
 public class DvrSeriesDeletionActivity extends Activity {
+    private static final String TAG = "DvrSeriesDeletionActivity";
+
     /** Name of series id added to the Intent. */
     public static final String SERIES_RECORDING_ID = "series_recording_id";
 
+    public static final int REQUEST_DELETE = 1;
+    public static final long INVALID_SERIES_RECORDING_ID = -1;
+
+    private long mSeriesRecordingId = INVALID_SERIES_RECORDING_ID;
+    private final List<Long> mIdsToDelete = new ArrayList<>();
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
         Starter.start(this);
@@ -34,9 +52,61 @@
         setContentView(R.layout.activity_dvr_series_settings);
         // Check savedInstanceState to prevent that activity is being showed with animation.
         if (savedInstanceState == null) {
+            mSeriesRecordingId =
+                    getIntent().getLongExtra(SERIES_RECORDING_ID, INVALID_SERIES_RECORDING_ID);
             DvrSeriesDeletionFragment deletionFragment = new DvrSeriesDeletionFragment();
             deletionFragment.setArguments(getIntent().getExtras());
             GuidedStepFragment.addAsRoot(this, deletionFragment, R.id.dvr_settings_view_frame);
         }
     }
+
+    @Override
+    public void onRequestPermissionsResult(
+            int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+        switch (requestCode) {
+            case REQUEST_DELETE:
+                // If request is cancelled, the result arrays are empty.
+                if (grantResults.length > 0
+                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+                    deleteSelectedIds(true);
+                } else {
+                    // NOTE: If Live TV ever supports both embedded and separate DVR inputs
+                    // then we should try to do the delete regardless.
+                    Log.i(
+                            TAG,
+                            "Write permission denied, Not trying to delete the files for series "
+                                    + mSeriesRecordingId);
+                    deleteSelectedIds(false);
+                }
+                break;
+            default:
+                super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+        }
+    }
+
+    private void deleteSelectedIds(boolean deleteFiles) {
+        TvSingletons singletons = TvSingletons.getSingletons(this);
+        int recordingSize =
+                singletons.getDvrDataManager().getRecordedPrograms(mSeriesRecordingId).size();
+        if (!mIdsToDelete.isEmpty()) {
+            DvrManager dvrManager = singletons.getDvrManager();
+            dvrManager.removeRecordedPrograms(mIdsToDelete, deleteFiles);
+        }
+        Toast.makeText(
+                this,
+                getResources()
+                        .getQuantityString(
+                                R.plurals.dvr_msg_episodes_deleted,
+                                mIdsToDelete.size(),
+                                mIdsToDelete.size(),
+                                recordingSize),
+                Toast.LENGTH_LONG)
+                .show();
+        finish();
+    }
+
+    void setIdsToDelete(List<Long> ids) {
+        mIdsToDelete.clear();
+        mIdsToDelete.addAll(ids);
+    }
 }
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java
index 685f0a5..ff21323 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java
@@ -29,6 +29,7 @@
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
 import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.util.PermissionUtils;
 import com.android.tv.dvr.DvrDataManager;
 import com.android.tv.dvr.DvrManager;
 import com.android.tv.dvr.DvrWatchedPositionManager;
@@ -53,10 +54,12 @@
     private static final long ACTION_ID_SELECT_ALL = -111;
     private static final long ACTION_ID_DELETE = -112;
 
+    private DvrManager mDvrManager;
     private DvrDataManager mDvrDataManager;
     private DvrWatchedPositionManager mDvrWatchedPositionManager;
     private List<RecordedProgram> mRecordings;
     private final Set<Long> mWatchedRecordings = new HashSet<>();
+    private final List<Long> mIdsToDelete = new ArrayList<>();
     private boolean mAllSelected;
     private long mSeriesRecordingId;
     private int mOneLineActionHeight;
@@ -67,9 +70,10 @@
         mSeriesRecordingId =
                 getArguments().getLong(DvrSeriesDeletionActivity.SERIES_RECORDING_ID, -1);
         SoftPreconditions.checkArgument(mSeriesRecordingId != -1);
-        mDvrDataManager = TvSingletons.getSingletons(context).getDvrDataManager();
-        mDvrWatchedPositionManager =
-                TvSingletons.getSingletons(context).getDvrWatchedPositionManager();
+        TvSingletons singletons = TvSingletons.getSingletons(context);
+        mDvrManager = singletons.getDvrManager();
+        mDvrDataManager = singletons.getDvrDataManager();
+        mDvrWatchedPositionManager = singletons.getDvrWatchedPositionManager();
         mRecordings = mDvrDataManager.getRecordedPrograms(mSeriesRecordingId);
         mOneLineActionHeight =
                 getResources()
@@ -158,28 +162,7 @@
     public void onGuidedActionClicked(GuidedAction action) {
         long actionId = action.getId();
         if (actionId == ACTION_ID_DELETE) {
-            List<Long> idsToDelete = new ArrayList<>();
-            for (GuidedAction guidedAction : getActions()) {
-                if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID
-                        && guidedAction.isChecked()) {
-                    idsToDelete.add(guidedAction.getId());
-                }
-            }
-            if (!idsToDelete.isEmpty()) {
-                DvrManager dvrManager = TvSingletons.getSingletons(getActivity()).getDvrManager();
-                dvrManager.removeRecordedPrograms(idsToDelete);
-            }
-            Toast.makeText(
-                            getContext(),
-                            getResources()
-                                    .getQuantityString(
-                                            R.plurals.dvr_msg_episodes_deleted,
-                                            idsToDelete.size(),
-                                            idsToDelete.size(),
-                                            mRecordings.size()),
-                            Toast.LENGTH_LONG)
-                    .show();
-            finishGuidedStepFragments();
+            delete();
         } else if (actionId == GuidedAction.ACTION_ID_CANCEL) {
             finishGuidedStepFragments();
         } else if (actionId == ACTION_ID_SELECT_WATCHED) {
@@ -234,6 +217,51 @@
         };
     }
 
+    private void delete() {
+        mIdsToDelete.clear();
+        for (GuidedAction guidedAction : getActions()) {
+            if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID
+                    && guidedAction.isChecked()) {
+                mIdsToDelete.add(guidedAction.getId());
+            }
+        }
+        ((DvrSeriesDeletionActivity) getActivity()).setIdsToDelete(mIdsToDelete);
+        if (!PermissionUtils.hasWriteExternalStorage(getContext())
+                && doesAnySelectedRecordedProgramNeedWritePermission()) {
+            DvrUiHelper.showWriteStoragePermissionRationaleDialog(getActivity());
+        } else {
+            deleteSelectedIds();
+        }
+    }
+
+    private boolean doesAnySelectedRecordedProgramNeedWritePermission() {
+        for (RecordedProgram r : mRecordings) {
+            if (mIdsToDelete.contains(r.getId())
+                    && DvrManager.isFile(r.getDataUri())
+                    && !DvrManager.isFromBundledInput(r)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void deleteSelectedIds() {
+        if (!mIdsToDelete.isEmpty()) {
+            mDvrManager.removeRecordedPrograms(mIdsToDelete, true);
+        }
+        Toast.makeText(
+                        getContext(),
+                        getResources()
+                                .getQuantityString(
+                                        R.plurals.dvr_msg_episodes_deleted,
+                                        mIdsToDelete.size(),
+                                        mIdsToDelete.size(),
+                                        mRecordings.size()),
+                        Toast.LENGTH_LONG)
+                .show();
+        finishGuidedStepFragments();
+    }
+
     private String getWatchedString(long watchedPositionMs, long durationMs) {
         if (durationMs > WATCHED_TIME_UNIT_THRESHOLD) {
             return getResources()
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java
index edb62c9..c6e2685 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java
@@ -101,9 +101,9 @@
         String title = getString(R.string.dvr_series_recording_dialog_title);
         Drawable icon;
         if (!mHasConflict) {
-            icon = getResources().getDrawable(R.drawable.ic_check_circle_white_48dp, null);
+            icon = getResources().getDrawable(R.drawable.quantum_ic_check_circle_white_48, null);
         } else {
-            icon = getResources().getDrawable(R.drawable.ic_error_white_48dp, null);
+            icon = getResources().getDrawable(R.drawable.quantum_ic_error_white_48, null);
         }
         return new GuidanceStylist.Guidance(title, getDescription(), null, icon);
     }
diff --git a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
index e93387a..1ab4c50 100644
--- a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
@@ -126,7 +126,7 @@
         } else {
             description = getString(R.string.dvr_stop_recording_dialog_description);
         }
-        Drawable image = getResources().getDrawable(R.drawable.ic_warning_white_96dp, null);
+        Drawable image = getResources().getDrawable(R.drawable.quantum_ic_warning_white_96, null);
         return new Guidance(title, description, null, image);
     }
 
diff --git a/src/com/android/tv/dvr/ui/DvrUiHelper.java b/src/com/android/tv/dvr/ui/DvrUiHelper.java
index 16afbde..a121cf9 100644
--- a/src/com/android/tv/dvr/ui/DvrUiHelper.java
+++ b/src/com/android/tv/dvr/ui/DvrUiHelper.java
@@ -37,10 +37,10 @@
 import android.text.style.TextAppearanceSpan;
 import android.widget.ImageView;
 import android.widget.Toast;
+
 import com.android.tv.MainActivity;
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
-import com.android.tv.common.BuildConfig;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.recording.RecordingStorageStatusManager;
 import com.android.tv.common.util.CommonUtils;
@@ -57,7 +57,6 @@
 import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyScheduledDialogFragment;
 import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelRecordDurationOptionDialogFragment;
 import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelWatchConflictDialogFragment;
-import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrFutureProgramInfoDialogFragment;
 import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrInsufficientSpaceErrorDialogFragment;
 import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrMissingStorageErrorDialogFragment;
 import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrNoFreeSpaceErrorDialogFragment;
@@ -65,15 +64,17 @@
 import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrScheduleDialogFragment;
 import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrSmallSizedStorageErrorDialogFragment;
 import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrStopRecordingDialogFragment;
+import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrWriteStoragePermissionRationaleDialogFragment;
 import com.android.tv.dvr.ui.browse.DvrBrowseActivity;
-import com.android.tv.dvr.ui.browse.DvrDetailsActivity;
 import com.android.tv.dvr.ui.list.DvrHistoryActivity;
 import com.android.tv.dvr.ui.list.DvrSchedulesActivity;
 import com.android.tv.dvr.ui.list.DvrSchedulesFragment;
 import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment;
 import com.android.tv.dvr.ui.playback.DvrPlaybackActivity;
+import com.android.tv.ui.DetailsActivity;
 import com.android.tv.util.ToastUtils;
 import com.android.tv.util.Utils;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -241,13 +242,9 @@
     }
 
     /** Shows program information dialog. */
-    public static void showProgramInfoDialog(Activity activity, Program program) {
-        if (program == null || !BuildConfig.ENG) {
-            return;
-        }
-        Bundle args = new Bundle();
-        args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program);
-        showDialogFragment(activity, new DvrFutureProgramInfoDialogFragment(), args, false, true);
+    public static void showWriteStoragePermissionRationaleDialog(Activity activity) {
+        showDialogFragment(activity, new DvrWriteStoragePermissionRationaleDialogFragment(),
+                new Bundle(), false, false);
     }
 
     /**
@@ -577,47 +574,43 @@
         if (dvrItem == null) {
             return;
         }
-        Intent intent = new Intent(activity, DvrDetailsActivity.class);
+        Intent intent = new Intent(activity, DetailsActivity.class);
         long recordingId;
         int viewType;
         if (dvrItem instanceof ScheduledRecording) {
             ScheduledRecording schedule = (ScheduledRecording) dvrItem;
             recordingId = schedule.getId();
             if (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) {
-                viewType = DvrDetailsActivity.SCHEDULED_RECORDING_VIEW;
+                viewType = DetailsActivity.SCHEDULED_RECORDING_VIEW;
             } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
-                viewType = DvrDetailsActivity.CURRENT_RECORDING_VIEW;
+                viewType = DetailsActivity.CURRENT_RECORDING_VIEW;
             } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED
                     && schedule.getRecordedProgramId() != null) {
                 recordingId = schedule.getRecordedProgramId();
-                viewType = DvrDetailsActivity.RECORDED_PROGRAM_VIEW;
+                viewType = DetailsActivity.RECORDED_PROGRAM_VIEW;
             } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
-                viewType = DvrDetailsActivity.SCHEDULED_RECORDING_VIEW;
+                viewType = DetailsActivity.SCHEDULED_RECORDING_VIEW;
                 hideViewSchedule = true;
-                // TODO(b/72638385): pass detailed error message
-                intent.putExtra(
-                        DvrDetailsActivity.EXTRA_FAILED_MESSAGE,
-                        activity.getString(R.string.dvr_recording_failed));
             } else {
                 return;
             }
         } else if (dvrItem instanceof RecordedProgram) {
             recordingId = ((RecordedProgram) dvrItem).getId();
-            viewType = DvrDetailsActivity.RECORDED_PROGRAM_VIEW;
+            viewType = DetailsActivity.RECORDED_PROGRAM_VIEW;
         } else if (dvrItem instanceof SeriesRecording) {
             recordingId = ((SeriesRecording) dvrItem).getId();
-            viewType = DvrDetailsActivity.SERIES_RECORDING_VIEW;
+            viewType = DetailsActivity.SERIES_RECORDING_VIEW;
         } else {
             return;
         }
-        intent.putExtra(DvrDetailsActivity.RECORDING_ID, recordingId);
-        intent.putExtra(DvrDetailsActivity.DETAILS_VIEW_TYPE, viewType);
-        intent.putExtra(DvrDetailsActivity.HIDE_VIEW_SCHEDULE, hideViewSchedule);
+        intent.putExtra(DetailsActivity.RECORDING_ID, recordingId);
+        intent.putExtra(DetailsActivity.DETAILS_VIEW_TYPE, viewType);
+        intent.putExtra(DetailsActivity.HIDE_VIEW_SCHEDULE, hideViewSchedule);
         Bundle bundle = null;
         if (imageView != null) {
             bundle =
                     ActivityOptionsCompat.makeSceneTransitionAnimation(
-                                    activity, imageView, DvrDetailsActivity.SHARED_ELEMENT_NAME)
+                                    activity, imageView, DetailsActivity.SHARED_ELEMENT_NAME)
                             .toBundle();
         }
         activity.startActivity(intent, bundle);
diff --git a/src/com/android/tv/dvr/ui/DvrWriteStoragePermissionRationaleFragment.java b/src/com/android/tv/dvr/ui/DvrWriteStoragePermissionRationaleFragment.java
new file mode 100644
index 0000000..c93f583
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrWriteStoragePermissionRationaleFragment.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui;
+
+import android.app.Activity;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.support.v17.leanback.widget.GuidanceStylist;
+import android.support.v17.leanback.widget.GuidedAction;
+
+import com.android.tv.R;
+
+import java.util.List;
+
+/**
+ * A fragment which shows the rationale when requesting android.permission.WRITE_EXTERNAL_STORAGE.
+ */
+public class DvrWriteStoragePermissionRationaleFragment extends DvrGuidedStepFragment {
+    @Override
+    public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) {
+        Resources res = getContext().getResources();
+        String title = res.getString(R.string.write_storage_permission_rationale_title);
+        String description = res.getString(R.string.write_storage_permission_rationale_description);
+        return new GuidanceStylist.Guidance(title, description, null, null);
+    }
+
+    @Override
+    public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+        Activity activity = getActivity();
+        actions.add(
+                new GuidedAction.Builder(activity)
+                        .id(GuidedAction.ACTION_ID_OK)
+                        .title(android.R.string.ok)
+                        .build());
+    }
+
+    @Override
+    public void onTrackedGuidedActionClicked(GuidedAction action) {
+        dismissDialog();
+    }
+
+    @Override
+    public String getTrackerPrefix() {
+        return "DvrWriteStoragePermissionRationaleFragment";
+    }
+}
diff --git a/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java
index f3a6fea..41ace9a 100644
--- a/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java
+++ b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java
@@ -27,9 +27,11 @@
 import android.view.ViewGroup;
 import android.widget.Button;
 
-// This class is adapted from Leanback's library, which does not support action icon with one-line
-// label. This class modified its getPresenter method to support the above situation.
-class ActionPresenterSelector extends PresenterSelector {
+/**
+ * This class is adapted from Leanback's library, which does not support action icon with one-line
+ * label. This class modified its getPresenter method to support the above situation.
+ */
+public class ActionPresenterSelector extends PresenterSelector {
     private final Presenter mOneLineActionPresenter = new OneLineActionPresenter();
     private final Presenter mTwoLineActionPresenter = new TwoLineActionPresenter();
     private final Presenter[] mPresenters =
diff --git a/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java
index 7e7e1f7..8c311d6 100644
--- a/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java
@@ -18,23 +18,34 @@
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.media.tv.TvInputManager;
 import android.support.v17.leanback.widget.Action;
 import android.support.v17.leanback.widget.OnActionClickedListener;
 import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
+import com.android.tv.common.flags.has.HasConcurrentDvrPlaybackFlags;
 import com.android.tv.dialog.HalfSizedDialogFragment;
 import com.android.tv.dvr.DvrDataManager;
 import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.DvrWatchedPositionManager;
+import com.android.tv.dvr.data.RecordedProgram;
 import com.android.tv.dvr.data.ScheduledRecording;
 import com.android.tv.dvr.ui.DvrStopRecordingFragment;
 import com.android.tv.dvr.ui.DvrUiHelper;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
 
 /** {@link RecordingDetailsFragment} for current recording in DVR. */
 public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment {
     private static final int ACTION_STOP_RECORDING = 1;
+    private static final int ACTION_RESUME_PLAYING = 2;
+    private static final int ACTION_PLAY_FROM_BEGINNING = 3;
 
     private DvrDataManager mDvrDataManger;
+    private RecordedProgram mRecordedProgram;
+    private DvrWatchedPositionManager mDvrWatchedPositionManager;
+    private ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
+    private boolean mPaused;
     private final DvrDataManager.ScheduledRecordingListener mScheduledRecordingListener =
             new DvrDataManager.ScheduledRecordingListener() {
                 @Override
@@ -68,10 +79,32 @@
         super.onAttach(context);
         mDvrDataManger = TvSingletons.getSingletons(context).getDvrDataManager();
         mDvrDataManger.addScheduledRecordingListener(mScheduledRecordingListener);
+        mDvrWatchedPositionManager =
+                TvSingletons.getSingletons(getActivity()).getDvrWatchedPositionManager();
+        mConcurrentDvrPlaybackFlags = HasConcurrentDvrPlaybackFlags.fromContext(context);
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        if (mPaused) {
+            updateActions();
+            mPaused = false;
+        }
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        mPaused = true;
     }
 
     @Override
     protected SparseArrayObjectAdapter onCreateActionsAdapter() {
+        Long recordedProgramId = getRecording().getRecordedProgramId();
+        if (recordedProgramId != null) {
+            mRecordedProgram = mDvrDataManger.getRecordedProgram(recordedProgramId);
+        }
         SparseArrayObjectAdapter adapter =
                 new SparseArrayObjectAdapter(new ActionPresenterSelector());
         Resources res = getResources();
@@ -82,6 +115,35 @@
                         res.getString(R.string.dvr_detail_stop_recording),
                         null,
                         res.getDrawable(R.drawable.lb_ic_stop)));
+        if (mConcurrentDvrPlaybackFlags.enabled()
+                && mRecordedProgram != null
+                && mRecordedProgram.isPartial()) {
+            if (mDvrWatchedPositionManager.getWatchedStatus(mRecordedProgram)
+                    == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) {
+                adapter.set(
+                        ACTION_RESUME_PLAYING,
+                        new Action(
+                                ACTION_RESUME_PLAYING,
+                                res.getString(R.string.dvr_detail_resume_play),
+                                null,
+                                res.getDrawable(R.drawable.lb_ic_play)));
+                adapter.set(
+                        ACTION_PLAY_FROM_BEGINNING,
+                        new Action(
+                                ACTION_PLAY_FROM_BEGINNING,
+                                res.getString(R.string.dvr_detail_play_from_beginning),
+                                null,
+                                res.getDrawable(R.drawable.lb_ic_replay)));
+            } else {
+                adapter.set(
+                        ACTION_PLAY_FROM_BEGINNING,
+                        new Action(
+                                ACTION_PLAY_FROM_BEGINNING,
+                                res.getString(R.string.dvr_detail_watch),
+                                null,
+                                res.getDrawable(R.drawable.lb_ic_play)));
+            }
+        }
         return adapter;
     }
 
@@ -107,6 +169,13 @@
                                     }
                                 }
                             });
+                } else if (action.getId() == ACTION_RESUME_PLAYING) {
+                    startPlayback(
+                            mRecordedProgram,
+                            mDvrWatchedPositionManager.getWatchedPosition(
+                                    mRecordedProgram.getId()));
+                } else if (action.getId() == ACTION_PLAY_FROM_BEGINNING) {
+                    startPlayback(mRecordedProgram, TvInputManager.TIME_SHIFT_INVALID_TIME);
                 }
             }
         };
diff --git a/src/com/android/tv/dvr/ui/browse/DetailsContent.java b/src/com/android/tv/dvr/ui/browse/DetailsContent.java
index cba6293..e179743 100644
--- a/src/com/android/tv/dvr/ui/browse/DetailsContent.java
+++ b/src/com/android/tv/dvr/ui/browse/DetailsContent.java
@@ -22,6 +22,7 @@
 import android.text.TextUtils;
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
+import com.android.tv.data.Program;
 import com.android.tv.data.api.Channel;
 import com.android.tv.dvr.data.RecordedProgram;
 import com.android.tv.dvr.data.ScheduledRecording;
@@ -29,7 +30,7 @@
 import com.android.tv.dvr.ui.DvrUiHelper;
 
 /** A class for details content. */
-class DetailsContent {
+public class DetailsContent {
     /** Constant for invalid time. */
     public static final long INVALID_TIME = -1;
 
@@ -40,6 +41,7 @@
     private String mLogoImageUri;
     private String mBackgroundImageUri;
     private boolean mUsingChannelLogo;
+    private boolean mShowErrorMessage;
 
     static DetailsContent createFromRecordedProgram(
             Context context, RecordedProgram recordedProgram) {
@@ -59,6 +61,23 @@
                 .build(context);
     }
 
+    public static DetailsContent createFromProgram(Context context, Program program) {
+        return new DetailsContent.Builder()
+                .setChannelId(program.getChannelId())
+                .setProgramTitle(program.getTitle())
+                .setSeasonNumber(program.getSeasonNumber())
+                .setEpisodeNumber(program.getEpisodeNumber())
+                .setStartTimeUtcMillis(program.getStartTimeUtcMillis())
+                .setEndTimeUtcMillis(program.getEndTimeUtcMillis())
+                .setDescription(
+                        TextUtils.isEmpty(program.getLongDescription())
+                                ? program.getDescription()
+                                : program.getLongDescription())
+                .setPosterArtUri(program.getPosterArtUri())
+                .setThumbnailUri(program.getThumbnailUri())
+                .build(context);
+    }
+
     static DetailsContent createFromSeriesRecording(
             Context context, SeriesRecording seriesRecording) {
         return new DetailsContent.Builder()
@@ -79,37 +98,9 @@
                 TvSingletons.getSingletons(context)
                         .getChannelDataManager()
                         .getChannel(scheduledRecording.getChannelId());
-        String description =
-                !TextUtils.isEmpty(scheduledRecording.getProgramDescription())
-                        ? scheduledRecording.getProgramDescription()
-                        : scheduledRecording.getProgramLongDescription();
-        if (TextUtils.isEmpty(description)) {
-            description = channel != null ? channel.getDescription() : null;
-        }
-        return new DetailsContent.Builder()
-                .setChannelId(scheduledRecording.getChannelId())
-                .setProgramTitle(scheduledRecording.getProgramTitle())
-                .setSeasonNumber(scheduledRecording.getSeasonNumber())
-                .setEpisodeNumber(scheduledRecording.getEpisodeNumber())
-                .setStartTimeUtcMillis(scheduledRecording.getStartTimeMs())
-                .setEndTimeUtcMillis(scheduledRecording.getEndTimeMs())
-                .setDescription(description)
-                .setPosterArtUri(scheduledRecording.getProgramPosterArtUri())
-                .setThumbnailUri(scheduledRecording.getProgramThumbnailUri())
-                .build(context);
-    }
-
-    static DetailsContent createFromFailedScheduledRecording(
-            Context context, ScheduledRecording scheduledRecording, String errMsg) {
-        Channel channel =
-                TvSingletons.getSingletons(context)
-                        .getChannelDataManager()
-                        .getChannel(scheduledRecording.getChannelId());
         String description;
-        if (scheduledRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED
-                && errMsg != null) {
-            description = errMsg
-                    + " (Error code: " + scheduledRecording.getFailedReason() + ")";
+        if (scheduledRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
+            description = getErrorMessage(context, scheduledRecording);
         } else {
             description =
                     !TextUtils.isEmpty(scheduledRecording.getProgramDescription())
@@ -129,9 +120,39 @@
                 .setDescription(description)
                 .setPosterArtUri(scheduledRecording.getProgramPosterArtUri())
                 .setThumbnailUri(scheduledRecording.getProgramThumbnailUri())
+                .setShowErrorMessage(
+                        scheduledRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED)
                 .build(context);
     }
 
+    private static String getErrorMessage(Context context, ScheduledRecording recording) {
+        int reason = recording.getFailedReason() == null
+                ? ScheduledRecording.FAILED_REASON_OTHER
+                : recording.getFailedReason();
+        switch (reason) {
+            case ScheduledRecording.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED:
+                return context.getString(R.string.dvr_recording_failed_not_started);
+            case ScheduledRecording.FAILED_REASON_RESOURCE_BUSY:
+                return context.getString(R.string.dvr_recording_failed_resource_busy);
+            case ScheduledRecording.FAILED_REASON_INPUT_UNAVAILABLE:
+                return context.getString(
+                        R.string.dvr_recording_failed_input_unavailable,
+                        recording.getInputId());
+            case ScheduledRecording.FAILED_REASON_INPUT_DVR_UNSUPPORTED:
+                return context.getString(R.string.dvr_recording_failed_input_dvr_unsupported);
+            case ScheduledRecording.FAILED_REASON_INSUFFICIENT_SPACE:
+                return context.getString(R.string.dvr_recording_failed_insufficient_space);
+            case ScheduledRecording.FAILED_REASON_OTHER: // fall through
+            case ScheduledRecording.FAILED_REASON_NOT_FINISHED: // fall through
+            case ScheduledRecording.FAILED_REASON_SCHEDULER_STOPPED: // fall through
+            case ScheduledRecording.FAILED_REASON_INVALID_CHANNEL: // fall through
+            case ScheduledRecording.FAILED_REASON_MESSAGE_NOT_SENT: // fall through
+            case ScheduledRecording.FAILED_REASON_CONNECTION_FAILED: // fall through
+            default:
+                return context.getString(R.string.dvr_recording_failed_system_failure, reason);
+        }
+    }
+
     private DetailsContent() {}
 
     /** Returns title. */
@@ -169,6 +190,11 @@
         return mUsingChannelLogo;
     }
 
+    /** Returns if the error message should be shown. */
+    public boolean shouldShowErrorMessage() {
+        return mShowErrorMessage;
+    }
+
     /** Copies other details content. */
     public void copyFrom(DetailsContent other) {
         if (this == other) {
@@ -181,6 +207,7 @@
         mLogoImageUri = other.mLogoImageUri;
         mBackgroundImageUri = other.mBackgroundImageUri;
         mUsingChannelLogo = other.mUsingChannelLogo;
+        mShowErrorMessage = other.mShowErrorMessage;
     }
 
     /** A class for building details content. */
@@ -266,6 +293,11 @@
             return this;
         }
 
+        private Builder setShowErrorMessage(boolean showErrorMessage) {
+            mDetailsContent.mShowErrorMessage = showErrorMessage;
+            return this;
+        }
+
         private void createStyledTitle(Context context, Channel channel) {
             CharSequence title =
                     DvrUiHelper.getStyledTitleWithEpisodeNumber(
diff --git a/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java
index aec8c41..6b5fd1f 100644
--- a/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java
+++ b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java
@@ -45,12 +45,13 @@
  * The latter class are re-used to provide a customized version of {@link
  * android.support.v17.leanback.widget.DetailsOverviewRow}.
  */
-class DetailsContentPresenter extends Presenter {
+public class DetailsContentPresenter extends Presenter {
     /** The ViewHolder for the {@link DetailsContentPresenter}. */
     public static class ViewHolder extends Presenter.ViewHolder {
         final TextView mTitle;
         final TextView mSubtitle;
         final LinearLayout mDescriptionContainer;
+        final LinearLayout mErrorMessage;
         final TextView mBody;
         final TextView mReadMoreView;
         final int mTitleMargin;
@@ -150,6 +151,8 @@
                     });
             mTitle = (TextView) view.findViewById(R.id.dvr_details_description_title);
             mSubtitle = (TextView) view.findViewById(R.id.dvr_details_description_subtitle);
+            mErrorMessage =
+                    (LinearLayout) view.findViewById(R.id.dvr_details_description_error_message);
             mBody = (TextView) view.findViewById(R.id.dvr_details_description_body);
             mDescriptionContainer =
                     (LinearLayout) view.findViewById(R.id.dvr_details_description_container);
@@ -321,6 +324,9 @@
         if (TextUtils.isEmpty(detailsContent.getDescription())) {
             vh.mBody.setVisibility(View.GONE);
         } else {
+            if (detailsContent.shouldShowErrorMessage()) {
+                vh.mErrorMessage.setVisibility(View.VISIBLE);
+            }
             vh.mBody.setText(detailsContent.getDescription());
             vh.mBody.setVisibility(View.VISIBLE);
             vh.mBody.setLineSpacing(
diff --git a/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java
index 849360b..4e41dae 100644
--- a/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java
+++ b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java
@@ -24,7 +24,7 @@
 import android.support.v17.leanback.app.BackgroundManager;
 
 /** The Background Helper. */
-class DetailsViewBackgroundHelper {
+public class DetailsViewBackgroundHelper {
     // Background delay serves to avoid kicking off expensive bitmap loading
     // in case multiple backgrounds are set in quick succession.
     private static final int SET_BACKGROUND_DELAY_MS = 100;
diff --git a/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java
index 6cc1c7a..5743ea5 100644
--- a/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java
@@ -22,9 +22,15 @@
 import android.os.Bundle;
 import com.android.tv.R;
 import com.android.tv.Starter;
+import com.android.tv.perf.PerformanceMonitorManagerFactory;
 
 /** {@link android.app.Activity} for DVR UI. */
 public class DvrBrowseActivity extends Activity {
+
+    {
+        PerformanceMonitorManagerFactory.create().getStartupMeasure().onActivityInit();
+    }
+
     private DvrBrowseFragment mFragment;
 
     @Override
diff --git a/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java
index 40b3a1f..17ba193 100644
--- a/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java
@@ -31,9 +31,7 @@
 import android.util.Log;
 import android.view.View;
 import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
-
 import com.android.tv.R;
-import com.android.tv.TvFeatures;
 import com.android.tv.TvSingletons;
 import com.android.tv.data.GenreItems;
 import com.android.tv.dvr.DvrDataManager;
@@ -47,7 +45,7 @@
 import com.android.tv.dvr.data.ScheduledRecording;
 import com.android.tv.dvr.data.SeriesRecording;
 import com.android.tv.dvr.ui.SortedArrayAdapter;
-
+import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
@@ -66,7 +64,7 @@
     private static final String TAG = "DvrBrowseFragment";
     private static final boolean DEBUG = false;
 
-    private static final int MAX_RECENT_ITEM_COUNT = 10;
+    private static final int MAX_RECENT_ITEM_COUNT = 4;
     private static final int MAX_SCHEDULED_ITEM_COUNT = 4;
 
     private boolean mShouldShowScheduleRow;
@@ -104,93 +102,84 @@
             };
 
     private final Comparator<Object> RECORDED_PROGRAM_COMPARATOR =
-            new Comparator<Object>() {
-                @Override
-                public int compare(Object lhs, Object rhs) {
-                    if (lhs instanceof SeriesRecording) {
-                        lhs = mSeriesId2LatestProgram.get(((SeriesRecording) lhs).getSeriesId());
-                    }
-                    if (rhs instanceof SeriesRecording) {
-                        rhs = mSeriesId2LatestProgram.get(((SeriesRecording) rhs).getSeriesId());
-                    }
-                    if (lhs instanceof RecordedProgram) {
-                        if (rhs instanceof RecordedProgram) {
-                            return RecordedProgram.START_TIME_THEN_ID_COMPARATOR
-                                    .reversed()
-                                    .compare((RecordedProgram) lhs, (RecordedProgram) rhs);
-                        } else {
-                            return -1;
-                        }
-                    } else if (rhs instanceof RecordedProgram) {
-                        return 1;
+            (Object lhs, Object rhs) -> {
+                if (lhs instanceof SeriesRecording) {
+                    lhs = mSeriesId2LatestProgram.get(((SeriesRecording) lhs).getSeriesId());
+                }
+                if (rhs instanceof SeriesRecording) {
+                    rhs = mSeriesId2LatestProgram.get(((SeriesRecording) rhs).getSeriesId());
+                }
+                if (lhs instanceof RecordedProgram) {
+                    if (rhs instanceof RecordedProgram) {
+                        return RecordedProgram.START_TIME_THEN_ID_COMPARATOR
+                                .reversed()
+                                .compare((RecordedProgram) lhs, (RecordedProgram) rhs);
                     } else {
-                        return 0;
+                        return -1;
                     }
+                } else if (rhs instanceof RecordedProgram) {
+                    return 1;
+                } else {
+                    return 0;
                 }
             };
 
     private static final Comparator<Object> SCHEDULE_COMPARATOR =
-            new Comparator<Object>() {
-                @Override
-                public int compare(Object lhs, Object rhs) {
-                    if (lhs instanceof ScheduledRecording) {
-                        if (rhs instanceof ScheduledRecording) {
-                            return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR
-                                    .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs);
-                        } else {
-                            return -1;
-                        }
-                    } else if (rhs instanceof ScheduledRecording) {
-                        return 1;
+            (Object lhs, Object rhs) -> {
+                if (lhs instanceof ScheduledRecording) {
+                    if (rhs instanceof ScheduledRecording) {
+                        return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR
+                                .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs);
                     } else {
-                        return 0;
+                        return -1;
                     }
+                } else if (rhs instanceof ScheduledRecording) {
+                    return 1;
+                } else {
+                    return 0;
                 }
             };
 
     static final Comparator<Object> RECENT_ROW_COMPARATOR =
-            new Comparator<Object>() {
-                @Override
-                public int compare(Object lhs, Object rhs) {
-                    if (lhs instanceof ScheduledRecording) {
-                        if (rhs instanceof ScheduledRecording) {
-                            return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR
-                                    .reversed()
-                                    .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs);
-                        } else if (rhs instanceof RecordedProgram) {
-                            ScheduledRecording scheduled = (ScheduledRecording) lhs;
-                            RecordedProgram recorded = (RecordedProgram) rhs;
-                            int compare =
-                                    Long.compare(
-                                            recorded.getStartTimeUtcMillis(),
-                                            scheduled.getStartTimeMs());
-                            // recorded program first when the start times are the same
-                            return compare == 0 ? 1 : compare;
-                        } else {
-                            return -1;
-                        }
-                    } else if (lhs instanceof RecordedProgram) {
-                        if (rhs instanceof RecordedProgram) {
-                            return RecordedProgram.START_TIME_THEN_ID_COMPARATOR
-                                    .reversed()
-                                    .compare((RecordedProgram) lhs, (RecordedProgram) rhs);
-                        } else if (rhs instanceof ScheduledRecording) {
-                            RecordedProgram recorded = (RecordedProgram) lhs;
-                            ScheduledRecording scheduled = (ScheduledRecording) rhs;
-                            int compare =
-                                    Long.compare(
-                                            scheduled.getStartTimeMs(),
-                                            recorded.getStartTimeUtcMillis());
-                            // recorded program first when the start times are the same
-                            return compare == 0 ? -1 : compare;
-                        } else {
-                            return -1;
-                        }
+            (Object lhs, Object rhs) -> {
+                if (lhs instanceof ScheduledRecording) {
+                    if (rhs instanceof ScheduledRecording) {
+                        return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR
+                                .reversed()
+                                .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs);
+                    } else if (rhs instanceof RecordedProgram) {
+                        ScheduledRecording scheduled = (ScheduledRecording) lhs;
+                        RecordedProgram recorded = (RecordedProgram) rhs;
+                        int compare =
+                                Long.compare(
+                                        recorded.getStartTimeUtcMillis(),
+                                        scheduled.getStartTimeMs());
+                        // recorded program first when the start times are the same
+                        return compare == 0 ? 1 : compare;
                     } else {
-                        return !(rhs instanceof RecordedProgram)
-                                && !(rhs instanceof ScheduledRecording)
-                                ? 0 : 1;
+                        return -1;
                     }
+                } else if (lhs instanceof RecordedProgram) {
+                    if (rhs instanceof RecordedProgram) {
+                        return RecordedProgram.START_TIME_THEN_ID_COMPARATOR
+                                .reversed()
+                                .compare((RecordedProgram) lhs, (RecordedProgram) rhs);
+                    } else if (rhs instanceof ScheduledRecording) {
+                        RecordedProgram recorded = (RecordedProgram) lhs;
+                        ScheduledRecording scheduled = (ScheduledRecording) rhs;
+                        int compare =
+                                Long.compare(
+                                        scheduled.getStartTimeMs(),
+                                        recorded.getStartTimeUtcMillis());
+                        // recorded program first when the start times are the same
+                        return compare == 0 ? -1 : compare;
+                    } else {
+                        return -1;
+                    }
+                } else {
+                    return !(rhs instanceof RecordedProgram) && !(rhs instanceof ScheduledRecording)
+                            ? 0
+                            : 1;
                 }
             };
 
@@ -207,13 +196,7 @@
                 }
             };
 
-    private final Runnable mUpdateRowsRunnable =
-            new Runnable() {
-                @Override
-                public void run() {
-                    updateRows();
-                }
-            };
+    private final Runnable mUpdateRowsRunnable = this::updateRows;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
@@ -233,13 +216,10 @@
                                 SeriesRecording.class, new SeriesRecordingPresenter(context))
                         .addClassPresenter(
                                 FullScheduleCardHolder.class,
-                                new FullSchedulesCardPresenter(context));
+                                new FullSchedulesCardPresenter(context))
+                        .addClassPresenter(
+                                DvrHistoryCardHolder.class, new DvrHistoryCardPresenter(context));
 
-        if (TvFeatures.DVR_FAILED_LIST.isEnabled(context)) {
-            mPresenterSelector.addClassPresenter(
-                                DvrHistoryCardHolder.class,
-                                new DvrHistoryCardPresenter(context));
-        }
         mGenreLabels = new ArrayList<>(Arrays.asList(GenreItems.getLabels(context)));
         mGenreLabels.add(getString(R.string.dvr_main_others));
         prepareUiElements();
@@ -310,7 +290,9 @@
     @Override
     public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) {
         for (RecordedProgram recordedProgram : recordedPrograms) {
-            handleRecordedProgramChanged(recordedProgram);
+            if (recordedProgram.isVisible()) {
+                handleRecordedProgramChanged(recordedProgram);
+            }
         }
         postUpdateRows();
     }
@@ -340,6 +322,9 @@
     public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
         for (ScheduledRecording scheduleRecording : scheduledRecordings) {
             mScheduleAdapter.remove(scheduleRecording);
+            if (scheduleRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
+                mRecentAdapter.remove(scheduleRecording);
+            }
         }
     }
 
@@ -351,6 +336,9 @@
             } else {
                 mScheduleAdapter.removeWithId(scheduleRecording);
             }
+            if (scheduleRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
+                mRecentAdapter.change(scheduleRecording);
+            }
         }
     }
 
@@ -443,16 +431,17 @@
             mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER);
             // Recorded Programs.
             for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) {
-                handleRecordedProgramAdded(recordedProgram, false);
-            }
-            if (TvFeatures.DVR_FAILED_LIST.isEnabled(getContext())) {
-                // only get failed recordings
-                for (ScheduledRecording scheduledRecording
-                        : mDvrDataManager.getFailedScheduledRecordings()) {
-                    onScheduledRecordingAdded(scheduledRecording);
+                if (recordedProgram.isVisible()) {
+                    handleRecordedProgramAdded(recordedProgram, false);
                 }
-                mRecentAdapter.addExtraItem(DvrHistoryCardHolder.DVR_HISTORY_CARD_HOLDER);
             }
+            // only get failed recordings
+            for (ScheduledRecording scheduledRecording :
+                    mDvrDataManager.getFailedScheduledRecordings()) {
+                onScheduledRecordingAdded(scheduledRecording);
+            }
+            mRecentAdapter.addExtraItem(DvrHistoryCardHolder.DVR_HISTORY_CARD_HOLDER);
+
             // Series Recordings. Series recordings should be added after recorded programs, because
             // we build series recordings' latest program information while adding recorded
             // programs.
@@ -592,9 +581,9 @@
         }
     }
 
-    private List<RecordedProgramAdapter> getGenreAdapters(String[] genres) {
+    private List<RecordedProgramAdapter> getGenreAdapters(ImmutableList<String> genres) {
         List<RecordedProgramAdapter> result = new ArrayList<>();
-        if (genres == null || genres.length == 0) {
+        if (genres == null || genres.isEmpty()) {
             result.add(mGenreAdapters[mGenreAdapters.length - 1]);
         } else {
             for (String genre : genres) {
@@ -642,8 +631,8 @@
 
     private void updateRows() {
         int visibleRowsCount = 1; // Schedule's Row will never be empty
-        int recentRowMinSize = TvFeatures.DVR_FAILED_LIST.isEnabled(getContext()) ? 1 : 0;
-        if (mRecentAdapter.size() <= recentRowMinSize) {
+        if (mRecentAdapter.size() <= 1) {
+            // remove the row if there is only the DVR history card
             mRowsAdapter.remove(mRecentRow);
         } else {
             if (mRowsAdapter.indexOf(mRecentRow) < 0) {
@@ -673,6 +662,9 @@
                 }
             }
         }
+        if (getSelectedPosition() >= mRowsAdapter.size()) {
+            setSelectedPosition(mRecentAdapter.size() - 1);
+        }
     }
 
     private boolean needToShowScheduledRecording(ScheduledRecording recording) {
@@ -713,16 +705,13 @@
         SeriesAdapter() {
             super(
                     mPresenterSelector,
-                    new Comparator<SeriesRecording>() {
-                        @Override
-                        public int compare(SeriesRecording lhs, SeriesRecording rhs) {
-                            if (lhs.isStopped() && !rhs.isStopped()) {
-                                return 1;
-                            } else if (!lhs.isStopped() && rhs.isStopped()) {
-                                return -1;
-                            }
-                            return SeriesRecording.PRIORITY_COMPARATOR.compare(lhs, rhs);
+                    (SeriesRecording lhs, SeriesRecording rhs) -> {
+                        if (lhs.isStopped() && !rhs.isStopped()) {
+                            return 1;
+                        } else if (!lhs.isStopped() && rhs.isStopped()) {
+                            return -1;
                         }
+                        return SeriesRecording.PRIORITY_COMPARATOR.compare(lhs, rhs);
                     });
         }
 
diff --git a/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java
deleted file mode 100644
index 0336b31..0000000
--- a/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.tv.dvr.ui.browse;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.support.v17.leanback.app.DetailsFragment;
-import android.transition.Transition;
-import android.transition.Transition.TransitionListener;
-import android.view.View;
-import com.android.tv.R;
-import com.android.tv.Starter;
-import com.android.tv.dialog.PinDialogFragment;
-
-/** Activity to show details view in DVR. */
-public class DvrDetailsActivity extends Activity implements PinDialogFragment.OnPinCheckedListener {
-    /** Name of record id added to the Intent. */
-    public static final String RECORDING_ID = "record_id";
-
-    /**
-     * Name of flag added to the Intent to determine if details view should hide "View schedule"
-     * button.
-     */
-    public static final String HIDE_VIEW_SCHEDULE = "hide_view_schedule";
-
-    /** Name of details view's type added to the intent. */
-    public static final String DETAILS_VIEW_TYPE = "details_view_type";
-
-    /** Name of shared element between activities. */
-    public static final String SHARED_ELEMENT_NAME = "shared_element";
-
-    /** Name of error message of a failed recording */
-    public static final String EXTRA_FAILED_MESSAGE = "failed_message";
-
-    /** CURRENT_RECORDING_VIEW refers to Current Recordings in DVR. */
-    public static final int CURRENT_RECORDING_VIEW = 1;
-
-    /** SCHEDULED_RECORDING_VIEW refers to Scheduled Recordings in DVR. */
-    public static final int SCHEDULED_RECORDING_VIEW = 2;
-
-    /** RECORDED_PROGRAM_VIEW refers to Recorded programs in DVR. */
-    public static final int RECORDED_PROGRAM_VIEW = 3;
-
-    /** SERIES_RECORDING_VIEW refers to series recording in DVR. */
-    public static final int SERIES_RECORDING_VIEW = 4;
-
-    private PinDialogFragment.OnPinCheckedListener mOnPinCheckedListener;
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        Starter.start(this);
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.activity_dvr_details);
-        long recordId = getIntent().getLongExtra(RECORDING_ID, -1);
-        int detailsViewType = getIntent().getIntExtra(DETAILS_VIEW_TYPE, -1);
-        boolean hideViewSchedule = getIntent().getBooleanExtra(HIDE_VIEW_SCHEDULE, false);
-        String failedMsg = getIntent().getStringExtra(EXTRA_FAILED_MESSAGE);
-        if (recordId != -1 && detailsViewType != -1 && savedInstanceState == null) {
-            Bundle args = new Bundle();
-            args.putLong(RECORDING_ID, recordId);
-            DetailsFragment detailsFragment = null;
-            if (detailsViewType == CURRENT_RECORDING_VIEW) {
-                detailsFragment = new CurrentRecordingDetailsFragment();
-            } else if (detailsViewType == SCHEDULED_RECORDING_VIEW) {
-                args.putBoolean(HIDE_VIEW_SCHEDULE, hideViewSchedule);
-                args.putString(EXTRA_FAILED_MESSAGE, failedMsg);
-                detailsFragment = new ScheduledRecordingDetailsFragment();
-            } else if (detailsViewType == RECORDED_PROGRAM_VIEW) {
-                detailsFragment = new RecordedProgramDetailsFragment();
-            } else if (detailsViewType == SERIES_RECORDING_VIEW) {
-                detailsFragment = new SeriesRecordingDetailsFragment();
-            }
-            detailsFragment.setArguments(args);
-            getFragmentManager()
-                    .beginTransaction()
-                    .replace(R.id.dvr_details_view_frame, detailsFragment)
-                    .commit();
-        }
-
-        // This is a workaround for the focus on O device
-        addTransitionListener();
-    }
-
-    @Override
-    public void onPinChecked(boolean checked, int type, String rating) {
-        if (mOnPinCheckedListener != null) {
-            mOnPinCheckedListener.onPinChecked(checked, type, rating);
-        }
-    }
-
-    void setOnPinCheckListener(PinDialogFragment.OnPinCheckedListener listener) {
-        mOnPinCheckedListener = listener;
-    }
-
-    private void addTransitionListener() {
-        getWindow()
-                .getSharedElementEnterTransition()
-                .addListener(
-                        new TransitionListener() {
-                            @Override
-                            public void onTransitionStart(Transition transition) {
-                                // Do nothing
-                            }
-
-                            @Override
-                            public void onTransitionEnd(Transition transition) {
-                                View actions = findViewById(R.id.details_overview_actions);
-                                if (actions != null) {
-                                    actions.requestFocus();
-                                }
-                            }
-
-                            @Override
-                            public void onTransitionCancel(Transition transition) {
-                                // Do nothing
-
-                            }
-
-                            @Override
-                            public void onTransitionPause(Transition transition) {
-                                // Do nothing
-                            }
-
-                            @Override
-                            public void onTransitionResume(Transition transition) {
-                                // Do nothing
-                            }
-                        });
-    }
-}
diff --git a/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java
index 8f4e4da..f90981f 100644
--- a/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java
@@ -47,8 +47,10 @@
 import com.android.tv.dvr.data.RecordedProgram;
 import com.android.tv.dvr.ui.DvrUiHelper;
 import com.android.tv.parental.ParentalControlSettings;
+import com.android.tv.ui.DetailsActivity;
 import com.android.tv.util.ToastUtils;
 import com.android.tv.util.images.ImageLoader;
+import com.google.common.collect.ImmutableList;
 import java.io.File;
 
 abstract class DvrDetailsFragment extends DetailsFragment {
@@ -89,7 +91,7 @@
         rowPresenter.setBackgroundColor(
                 getResources().getColor(R.color.common_tv_background, null));
         rowPresenter.setSharedElementEnterTransition(
-                getActivity(), DvrDetailsActivity.SHARED_ELEMENT_NAME);
+                getActivity(), DetailsActivity.SHARED_ELEMENT_NAME);
         rowPresenter.setOnActionClickedListener(onCreateOnActionClickedListener());
         mRowsAdapter = new ArrayObjectAdapter(onCreatePresenterSelector(rowPresenter));
         setAdapter(mRowsAdapter);
@@ -221,7 +223,7 @@
             checkPinToPlay(recordedProgram, seekTimeMs);
             return;
         }
-        TvContentRating[] ratings = recordedProgram.getContentRatings();
+        ImmutableList<TvContentRating> ratings = recordedProgram.getContentRatings();
         TvContentRating blockRatings = parental.getBlockedRating(ratings);
         if (blockRatings != null) {
             checkPinToPlay(recordedProgram, seekTimeMs);
@@ -245,15 +247,14 @@
     }
 
     private void checkPinToPlay(RecordedProgram recordedProgram, long seekTimeMs) {
-        SoftPreconditions.checkState(getActivity() instanceof DvrDetailsActivity);
-        if (getActivity() instanceof DvrDetailsActivity) {
-            ((DvrDetailsActivity) getActivity())
+        SoftPreconditions.checkState(getActivity() instanceof DetailsActivity);
+        if (getActivity() instanceof DetailsActivity) {
+            ((DetailsActivity) getActivity())
                     .setOnPinCheckListener(
                             new OnPinCheckedListener() {
                                 @Override
                                 public void onPinChecked(boolean checked, int type, String rating) {
-                                    ((DvrDetailsActivity) getActivity())
-                                            .setOnPinCheckListener(null);
+                                    ((DetailsActivity) getActivity()).setOnPinCheckListener(null);
                                     if (checked
                                             && type
                                                     == PinDialogFragment
diff --git a/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java
index 47b1a19..bf96354 100644
--- a/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java
@@ -24,10 +24,13 @@
 import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
+import com.android.tv.common.util.PermissionUtils;
 import com.android.tv.dvr.DvrDataManager;
 import com.android.tv.dvr.DvrManager;
 import com.android.tv.dvr.DvrWatchedPositionManager;
 import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.ui.DvrUiHelper;
+import com.android.tv.ui.DetailsActivity;
 
 /** {@link android.support.v17.leanback.app.DetailsFragment} for recorded program in DVR. */
 public class RecordedProgramDetailsFragment extends DvrDetailsFragment
@@ -80,7 +83,7 @@
 
     @Override
     protected boolean onLoadRecordingDetails(Bundle args) {
-        long recordedProgramId = args.getLong(DvrDetailsActivity.RECORDING_ID);
+        long recordedProgramId = args.getLong(DetailsActivity.RECORDING_ID);
         mRecordedProgram = mDvrDataManager.getRecordedProgram(recordedProgramId);
         return mRecordedProgram != null;
     }
@@ -138,15 +141,24 @@
                             mDvrWatchedPositionManager.getWatchedPosition(
                                     mRecordedProgram.getId()));
                 } else if (action.getId() == ACTION_DELETE_RECORDING) {
-                    DvrManager dvrManager =
-                            TvSingletons.getSingletons(getActivity()).getDvrManager();
-                    dvrManager.removeRecordedProgram(mRecordedProgram);
-                    getActivity().finish();
+                    delete();
                 }
             }
         };
     }
 
+    private void delete() {
+        if (!PermissionUtils.hasWriteExternalStorage(getContext())
+                && DvrManager.isFile(mRecordedProgram.getDataUri())
+                && !DvrManager.isFromBundledInput(mRecordedProgram)) {
+            DvrUiHelper.showWriteStoragePermissionRationaleDialog(getActivity());
+        } else {
+            DvrManager dvrManager = TvSingletons.getSingletons(getActivity()).getDvrManager();
+            dvrManager.removeRecordedProgram(mRecordedProgram, true);
+            getActivity().finish();
+        }
+    }
+
     @Override
     public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) {}
 
diff --git a/src/com/android/tv/dvr/ui/browse/RecordingCardView.java b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java
index fe3c52d..c83ceaf 100644
--- a/src/com/android/tv/dvr/ui/browse/RecordingCardView.java
+++ b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java
@@ -48,11 +48,10 @@
     private final int mImageWidth;
     private final int mImageHeight;
     private String mImageUri;
+    private final ImageView mContentIconView;
     private final TextView mMajorContentView;
     private final TextView mMinorContentView;
     private final ProgressBar mProgressBar;
-    private final View mAffiliatedIconContainer;
-    private final ImageView mAffiliatedIcon;
     private final Drawable mDefaultImage;
     private final FrameLayout mTitleArea;
     private final TextView mFoldedTitleView;
@@ -94,8 +93,7 @@
         mImageWidth = imageWidth;
         mImageHeight = imageHeight;
         mProgressBar = (ProgressBar) findViewById(R.id.recording_progress);
-        mAffiliatedIconContainer = findViewById(R.id.affiliated_icon_container);
-        mAffiliatedIcon = (ImageView) findViewById(R.id.affiliated_icon);
+        mContentIconView = (ImageView) findViewById(R.id.content_icon);
         mMajorContentView = (TextView) findViewById(R.id.content_major);
         mMinorContentView = (TextView) findViewById(R.id.content_minor);
         mTitleArea = (FrameLayout) findViewById(R.id.title_area);
@@ -184,6 +182,7 @@
     }
 
     void setContent(CharSequence majorContent, CharSequence minorContent) {
+        mContentIconView.setVisibility(View.GONE);
         if (!TextUtils.isEmpty(majorContent)) {
             mMajorContentView.setText(majorContent);
             mMajorContentView.setVisibility(View.VISIBLE);
@@ -198,6 +197,24 @@
         }
     }
 
+    void setRecordingFailedContent(Context context) {
+        mContentIconView.setVisibility(View.VISIBLE);
+        mContentIconView.setImageResource(R.drawable.ic_error_outline_pink_24dp);
+        mMajorContentView.setText(context.getString(R.string.dvr_recording_failed_no_period));
+        mMajorContentView.setVisibility(View.VISIBLE);
+        mMajorContentView.setTextColor(
+                getResources().getColor(R.color.dvr_recording_failed_text_color, null));
+    }
+
+    void setRecordingConflictContent(Context context) {
+        mContentIconView.setVisibility(View.VISIBLE);
+        mContentIconView.setImageResource(R.drawable.ic_warning_yellow_24dp);
+        mMajorContentView.setText(context.getString(R.string.dvr_recording_conflict));
+        mMajorContentView.setVisibility(View.VISIBLE);
+        mMajorContentView.setTextColor(
+                getResources().getColor(R.color.dvr_recording_conflict_text_color, null));
+    }
+
     /** Sets progress bar. If progress is {@code null}, hides progress bar. */
     void setProgressBar(Integer progress) {
         if (progress == null) {
@@ -245,19 +262,6 @@
     }
 
     /**
-     * Sets the affiliated icon of the card view, which will be displayed at the lower-right corner
-     * of the poster.
-     */
-    public void setAffiliatedIcon(int imageResId) {
-        if (imageResId > 0) {
-            mAffiliatedIconContainer.setVisibility(View.VISIBLE);
-            mAffiliatedIcon.setImageResource(imageResId);
-        } else {
-            mAffiliatedIconContainer.setVisibility(View.INVISIBLE);
-        }
-    }
-
-    /**
      * Sets the background image URI of the card view, which will be displayed as background when
      * the view is clicked and shows its details fragment.
      */
diff --git a/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java
index aa2ccf7..243681c 100644
--- a/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java
@@ -20,6 +20,7 @@
 import android.support.v17.leanback.app.DetailsFragment;
 import com.android.tv.TvSingletons;
 import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.ui.DetailsActivity;
 
 /** {@link DetailsFragment} for recordings in DVR. */
 abstract class RecordingDetailsFragment extends DvrDetailsFragment {
@@ -33,7 +34,7 @@
 
     @Override
     protected boolean onLoadRecordingDetails(Bundle args) {
-        long scheduledRecordingId = args.getLong(DvrDetailsActivity.RECORDING_ID);
+        long scheduledRecordingId = args.getLong(DetailsActivity.RECORDING_ID);
         mRecording =
                 TvSingletons.getSingletons(getContext())
                         .getDvrDataManager()
diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java
index 302b831..f08bb12 100644
--- a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java
@@ -21,10 +21,12 @@
 import android.support.v17.leanback.widget.Action;
 import android.support.v17.leanback.widget.OnActionClickedListener;
 import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
 import com.android.tv.dvr.DvrManager;
 import com.android.tv.dvr.ui.DvrUiHelper;
+import com.android.tv.ui.DetailsActivity;
 
 /** {@link RecordingDetailsFragment} for scheduled recording in DVR. */
 public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment {
@@ -34,14 +36,12 @@
     private DvrManager mDvrManager;
     private Action mScheduleAction;
     private boolean mHideViewSchedule;
-    private String mFailedMessage;
 
     @Override
     public void onCreate(Bundle savedInstance) {
         Bundle args = getArguments();
         mDvrManager = TvSingletons.getSingletons(getContext()).getDvrManager();
-        mHideViewSchedule = args.getBoolean(DvrDetailsActivity.HIDE_VIEW_SCHEDULE);
-        mFailedMessage = args.getString(DvrDetailsActivity.EXTRA_FAILED_MESSAGE);
+        mHideViewSchedule = args.getBoolean(DetailsActivity.HIDE_VIEW_SCHEDULE);
         super.onCreate(savedInstance);
     }
 
@@ -54,17 +54,6 @@
     }
 
     @Override
-    protected void onCreateInternal() {
-        if (mFailedMessage == null) {
-            super.onCreateInternal();
-            return;
-        }
-        setDetailsOverviewRow(
-                DetailsContent.createFromFailedScheduledRecording(
-                        getContext(), getScheduledRecording(), mFailedMessage));
-    }
-
-    @Override
     protected SparseArrayObjectAdapter onCreateActionsAdapter() {
         SparseArrayObjectAdapter adapter =
                 new SparseArrayObjectAdapter(new ActionPresenterSelector());
diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java
index 8e02868..3d27935 100644
--- a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java
+++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java
@@ -119,21 +119,17 @@
         DetailsContent details = DetailsContent.createFromScheduledRecording(mContext, recording);
         cardView.setTitle(details.getTitle());
         cardView.setImageUri(details.getLogoImageUri(), details.isUsingChannelLogo());
-        if (mDvrManager.isConflicting(recording)) {
-            cardView.setAffiliatedIcon(R.drawable.ic_warning_white_32dp);
-        } else if (recording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
-            cardView.setAffiliatedIcon(R.drawable.ic_error_white_48dp);
+        if (recording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
+            cardView.setRecordingFailedContent(mContext);
+        } else if (mDvrManager.isConflicting(recording)) {
+            cardView.setRecordingConflictContent(mContext);
         } else {
-            cardView.setAffiliatedIcon(0);
+            cardView.setContent(generateMajorContent(recording), null);
         }
-        cardView.setContent(generateMajorContent(recording), null);
         cardView.setDetailBackgroundImageUri(details.getBackgroundImageUri());
     }
 
     private String generateMajorContent(ScheduledRecording recording) {
-        if (recording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
-            return mContext.getString(R.string.dvr_recording_failed);
-        }
         int dateDifference =
                 Utils.computeDateDifference(System.currentTimeMillis(), recording.getStartTimeMs());
         if (dateDifference <= 0) {
diff --git a/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java
index 2cd191a..9104ef1 100644
--- a/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java
@@ -20,6 +20,7 @@
 import android.graphics.drawable.Drawable;
 import android.media.tv.TvInputManager;
 import android.os.Bundle;
+import android.support.annotation.Nullable;
 import android.support.v17.leanback.app.DetailsFragment;
 import android.support.v17.leanback.widget.Action;
 import android.support.v17.leanback.widget.ArrayObjectAdapter;
@@ -41,6 +42,7 @@
 import com.android.tv.dvr.data.SeriesRecording;
 import com.android.tv.dvr.ui.DvrUiHelper;
 import com.android.tv.dvr.ui.SortedArrayAdapter;
+import com.android.tv.ui.DetailsActivity;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
@@ -135,7 +137,7 @@
 
     @Override
     protected boolean onLoadRecordingDetails(Bundle args) {
-        long recordId = args.getLong(DvrDetailsActivity.RECORDING_ID);
+        long recordId = args.getLong(DetailsActivity.RECORDING_ID);
         mSeries =
                 TvSingletons.getSingletons(getActivity())
                         .getDvrDataManager()
@@ -215,6 +217,7 @@
     }
 
     /** The programs are sorted by season number and episode number. */
+    @Nullable
     private RecordedProgram getRecommendProgram(List<RecordedProgram> programs) {
         for (int i = programs.size() - 1; i >= 0; i--) {
             RecordedProgram program = programs.get(i);
@@ -289,7 +292,8 @@
                         }
                     }
                 }
-                if (recordedProgram.getId() == mRecommendRecordedProgram.getId()) {
+                if (mRecommendRecordedProgram != null
+                        && recordedProgram.getId() == mRecommendRecordedProgram.getId()) {
                     updateWatchAction();
                 }
             }
@@ -339,14 +343,7 @@
                 new ListRow(
                         header,
                         new SeasonRowAdapter(
-                                selector,
-                                new Comparator<RecordedProgram>() {
-                                    @Override
-                                    public int compare(RecordedProgram lhs, RecordedProgram rhs) {
-                                        return BaseProgram.EPISODE_COMPARATOR.compare(lhs, rhs);
-                                    }
-                                },
-                                seasonNumber));
+                                selector, BaseProgram.EPISODE_COMPARATOR::compare, seasonNumber));
         getRowsAdapter().add(position, row);
         return row;
     }
diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
index 38d3d58..11680a0 100644
--- a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
+++ b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
@@ -37,7 +37,6 @@
 import android.widget.TextView;
 import android.widget.Toast;
 import com.android.tv.R;
-import com.android.tv.TvFeatures;
 import com.android.tv.TvSingletons;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.data.api.Channel;
@@ -90,18 +89,19 @@
         private ScheduleRowPresenter mPresenter;
         @ScheduleRowAction private int[] mActions;
         private boolean mLtr;
-        private LinearLayout mInfoContainer;
+        private final LinearLayout mInfoContainer;
         // The first action is on the right of the second action.
-        private RelativeLayout mSecondActionContainer;
-        private RelativeLayout mFirstActionContainer;
-        private View mSelectorView;
-        private TextView mTimeView;
-        private TextView mProgramTitleView;
-        private TextView mInfoSeparatorView;
-        private TextView mChannelNameView;
-        private TextView mConflictInfoView;
-        private ImageView mSecondActionView;
-        private ImageView mFirstActionView;
+        private final RelativeLayout mSecondActionContainer;
+        private final RelativeLayout mFirstActionContainer;
+        private final View mSelectorView;
+        private final TextView mTimeView;
+        private final TextView mProgramTitleView;
+        private final TextView mInfoSeparatorView;
+        private final TextView mChannelNameView;
+        private final ImageView mExtraInfoIcon;
+        private final TextView mExtraInfoView;
+        private final ImageView mSecondActionView;
+        private final ImageView mFirstActionView;
 
         private Runnable mPendingAnimationRunnable;
 
@@ -117,14 +117,11 @@
                     @Override
                     public void onFocusChange(View view, boolean focused) {
                         view.post(
-                                new Runnable() {
-                                    @Override
-                                    public void run() {
-                                        if (view.isFocused()) {
-                                            mPresenter.mLastFocusedViewId = view.getId();
-                                        }
-                                        updateSelector();
+                                () -> {
+                                    if (view.isFocused()) {
+                                        mPresenter.mLastFocusedViewId = view.getId();
                                     }
+                                    updateSelector();
                                 });
                     }
                 };
@@ -146,7 +143,8 @@
             mProgramTitleView = (TextView) view.findViewById(R.id.program_title);
             mInfoSeparatorView = (TextView) view.findViewById(R.id.info_separator);
             mChannelNameView = (TextView) view.findViewById(R.id.channel_name);
-            mConflictInfoView = (TextView) view.findViewById(R.id.conflict_info);
+            mExtraInfoIcon = (ImageView) view.findViewById(R.id.extra_info_icon);
+            mExtraInfoView = (TextView) view.findViewById(R.id.extra_info);
             Resources res = view.getResources();
             mSelectorTranslationDelta =
                     res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin)
@@ -311,7 +309,7 @@
                     mInfoContainer
                             .getResources()
                             .getColor(R.color.dvr_schedules_item_info_grey, null));
-            mConflictInfoView.setTextColor(
+            mExtraInfoView.setTextColor(
                     mInfoContainer
                             .getResources()
                             .getColor(R.color.dvr_schedules_item_info_grey, null));
@@ -327,7 +325,7 @@
                     mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null));
             mChannelNameView.setTextColor(
                     mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null));
-            mConflictInfoView.setTextColor(
+            mExtraInfoView.setTextColor(
                     mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null));
         }
     }
@@ -426,39 +424,76 @@
             }
         }
         ScheduledRecording schedule = row.getSchedule();
-        if (mDvrManager.isConflicting(schedule)
-                || (TvFeatures.DVR_FAILED_LIST.isEnabled(getContext())
-                        && schedule != null
-                        && schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED)) {
-            String conflictInfo;
-            if (TvFeatures.DVR_FAILED_LIST.isEnabled(getContext())
-                    && schedule != null
-                    && schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
-                // TODO(b/72638385): show real error messages
-                // TODO(b/72638385): use a better name for ConflictInfoXXX
-                conflictInfo = "Failed";
-                if (schedule.getFailedReason() != null) {
-                    conflictInfo += " (Error code: " + schedule.getFailedReason() + ")";
-                }
+        viewHolder.mExtraInfoIcon.setVisibility(View.GONE);
+        if (mDvrManager.isConflicting(schedule) || isFailedRecording(schedule)) {
+            String extraInfo;
+            if (isFailedRecording(schedule)) {
+                extraInfo =
+                        mContext.getString(R.string.dvr_recording_failed_short)
+                                + " "
+                                + getErrorMessage(schedule);
+                viewHolder.mExtraInfoIcon.setVisibility(View.VISIBLE);
             } else if (mDvrScheduleManager.isPartiallyConflicting(row.getSchedule())) {
-                conflictInfo = mTunerConflictWillBePartiallyRecordedInfo;
+                extraInfo = mTunerConflictWillBePartiallyRecordedInfo;
             } else {
-                conflictInfo = mTunerConflictWillNotBeRecordedInfo;
+                extraInfo = mTunerConflictWillNotBeRecordedInfo;
             }
-            viewHolder.mConflictInfoView.setText(conflictInfo);
-            viewHolder.mConflictInfoView.setVisibility(View.VISIBLE);
+            viewHolder.mExtraInfoView.setText(extraInfo);
+            viewHolder.mExtraInfoView.setVisibility(View.VISIBLE);
         } else {
-            viewHolder.mConflictInfoView.setVisibility(View.GONE);
+            viewHolder.mExtraInfoView.setVisibility(View.GONE);
         }
         if (shouldBeGrayedOut(row)) {
             viewHolder.greyOutInfo();
         } else {
             viewHolder.whiteBackInfo();
         }
+        if (isFailedRecording(schedule)) {
+            viewHolder.mExtraInfoView.setTextColor(
+                    viewHolder
+                            .mInfoContainer
+                            .getResources()
+                            .getColor(R.color.dvr_recording_failed_text_color, null));
+        }
         viewHolder.mInfoContainer.setFocusable(isInfoClickable(row));
         updateActionContainer(viewHolder, viewHolder.isSelected());
     }
 
+    private boolean isFailedRecording(ScheduledRecording scheduledRecording) {
+        return scheduledRecording != null
+                && scheduledRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED;
+    }
+
+    private String getErrorMessage(ScheduledRecording recording) {
+        int reason =
+                recording.getFailedReason() == null
+                        ? ScheduledRecording.FAILED_REASON_OTHER
+                        : recording.getFailedReason();
+        switch (reason) {
+            case ScheduledRecording.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED:
+                return mContext.getString(R.string.dvr_recording_failed_not_started_short);
+            case ScheduledRecording.FAILED_REASON_RESOURCE_BUSY:
+                return mContext.getString(R.string.dvr_recording_failed_resource_busy_short);
+            case ScheduledRecording.FAILED_REASON_INPUT_UNAVAILABLE:
+                return mContext.getString(
+                        R.string.dvr_recording_failed_input_unavailable_short,
+                        recording.getInputId());
+            case ScheduledRecording.FAILED_REASON_INPUT_DVR_UNSUPPORTED:
+                return mContext.getString(
+                        R.string.dvr_recording_failed_input_dvr_unsupported_short);
+            case ScheduledRecording.FAILED_REASON_INSUFFICIENT_SPACE:
+                return mContext.getString(R.string.dvr_recording_failed_insufficient_space_short);
+            case ScheduledRecording.FAILED_REASON_OTHER: // fall through
+            case ScheduledRecording.FAILED_REASON_NOT_FINISHED: // fall through
+            case ScheduledRecording.FAILED_REASON_SCHEDULER_STOPPED: // fall through
+            case ScheduledRecording.FAILED_REASON_INVALID_CHANNEL: // fall through
+            case ScheduledRecording.FAILED_REASON_MESSAGE_NOT_SENT: // fall through
+            case ScheduledRecording.FAILED_REASON_CONNECTION_FAILED: // fall through
+            default:
+                return mContext.getString(R.string.dvr_recording_failed_system_failure, reason);
+        }
+    }
+
     private int getImageForAction(@ScheduleRowAction int action) {
         switch (action) {
             case ACTION_START_RECORDING:
@@ -512,7 +547,8 @@
         return schedule != null
                 && (schedule.isNotStarted()
                         || schedule.isInProgress()
-                        || schedule.isFinished());
+                        || schedule.isFinished()
+                        || schedule.isFailed());
     }
 
     /** Called when the button in a row is clicked. */
@@ -702,23 +738,17 @@
                     prepareShowActionView(viewHolder.mSecondActionContainer);
                     prepareShowActionView(viewHolder.mFirstActionContainer);
                     viewHolder.mPendingAnimationRunnable =
-                            new Runnable() {
-                                @Override
-                                public void run() {
-                                    showActionView(viewHolder.mSecondActionContainer);
-                                    showActionView(viewHolder.mFirstActionContainer);
-                                }
+                            () -> {
+                                showActionView(viewHolder.mSecondActionContainer);
+                                showActionView(viewHolder.mFirstActionContainer);
                             };
                     break;
                 case 1:
                     prepareShowActionView(viewHolder.mFirstActionContainer);
                     viewHolder.mPendingAnimationRunnable =
-                            new Runnable() {
-                                @Override
-                                public void run() {
-                                    hideActionView(viewHolder.mSecondActionContainer, View.GONE);
-                                    showActionView(viewHolder.mFirstActionContainer);
-                                }
+                            () -> {
+                                hideActionView(viewHolder.mSecondActionContainer, View.GONE);
+                                showActionView(viewHolder.mFirstActionContainer);
                             };
                     if (mLastFocusedViewId == R.id.action_second_container) {
                         mLastFocusedViewId = R.id.info_container;
@@ -727,12 +757,9 @@
                 case 0:
                 default:
                     viewHolder.mPendingAnimationRunnable =
-                            new Runnable() {
-                                @Override
-                                public void run() {
-                                    hideActionView(viewHolder.mSecondActionContainer, View.GONE);
-                                    hideActionView(viewHolder.mFirstActionContainer, View.GONE);
-                                }
+                            () -> {
+                                hideActionView(viewHolder.mSecondActionContainer, View.GONE);
+                                hideActionView(viewHolder.mFirstActionContainer, View.GONE);
                             };
                     mLastFocusedViewId = R.id.info_container;
                     SoftPreconditions.checkState(
diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
index eb01aba..28a44bf 100644
--- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
+++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
@@ -211,13 +211,7 @@
                         new View.OnFocusChangeListener() {
                             @Override
                             public void onFocusChange(View view, boolean focused) {
-                                view.post(
-                                        new Runnable() {
-                                            @Override
-                                            public void run() {
-                                                updateSelector(view);
-                                            }
-                                        });
+                                view.post(() -> updateSelector(view));
                             }
                         };
                 mSeriesSettingsButton.setOnFocusChangeListener(onFocusChangeListener);
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java
index b8b19ad..f24ad2c 100644
--- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java
@@ -74,8 +74,10 @@
     private Intent createProgramIntent(Intent intent) {
         if (Intent.ACTION_VIEW.equals(intent.getAction())) {
             Uri uri = intent.getData();
-            long recordedProgramId = ContentUris.parseId(uri);
-            intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, recordedProgramId);
+            if (uri != null) {
+                long recordedProgramId = ContentUris.parseId(uri);
+                intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, recordedProgramId);
+            }
         }
         return intent;
     }
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java
index 59c90d1..791d26b 100644
--- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java
@@ -39,6 +39,7 @@
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.View;
+import android.view.ViewGroup;
 import com.android.tv.R;
 import com.android.tv.util.TimeShiftUtils;
 import java.util.ArrayList;
@@ -53,10 +54,13 @@
     private static final boolean DEBUG = false;
 
     private static final int AUDIO_ACTION_ID = 1001;
+    private static final long INVALID_TIME = -1;
 
     private int mPlaybackState = PlaybackState.STATE_NONE;
     private int mPlaybackSpeedLevel;
     private int mPlaybackSpeedId;
+    private long mProgramStartTimeMs = INVALID_TIME;
+    private boolean mEnableBuffering = false;
     private boolean mReadyToControl;
 
     private final DvrPlaybackOverlayFragment mFragment;
@@ -67,6 +71,8 @@
     private final MultiAction mClosedCaptioningAction;
     private final MultiAction mMultiAudioAction;
     private ArrayObjectAdapter mSecondaryActionsAdapter;
+    private PlaybackControlsRow mPlaybackControlsRow;
+    @Nullable private View mPlayPauseButton;
 
     DvrPlaybackControlHelper(Activity activity, DvrPlaybackOverlayFragment overlayFragment) {
         super(activity, new int[TimeShiftUtils.MAX_SPEED_LEVEL + 1]);
@@ -79,13 +85,18 @@
                         .getDimensionPixelOffset(R.dimen.dvr_playback_controls_extra_padding_top);
         mClosedCaptioningAction = new ClosedCaptioningAction(activity);
         mMultiAudioAction = new MultiAudioAction(activity);
+        mProgramStartTimeMs = overlayFragment.getProgramStartTimeMs();
+        if (mProgramStartTimeMs != INVALID_TIME) {
+            mEnableBuffering = true;
+        }
         createControlsRowPresenter();
     }
 
     void createControlsRow() {
-        PlaybackControlsRow controlsRow = new PlaybackControlsRow(this);
-        setControlsRow(controlsRow);
-        mSecondaryActionsAdapter = (ArrayObjectAdapter) controlsRow.getSecondaryActionsAdapter();
+        mPlaybackControlsRow = new PlaybackControlsRow(this);
+        setControlsRow(mPlaybackControlsRow);
+        mSecondaryActionsAdapter =
+                (ArrayObjectAdapter) mPlaybackControlsRow.getSecondaryActionsAdapter();
     }
 
     private void createControlsRowPresenter() {
@@ -118,6 +129,8 @@
                     protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) {
                         super.onBindRowViewHolder(vh, item);
                         vh.setOnKeyListener(DvrPlaybackControlHelper.this);
+                        ViewGroup controlBar = (ViewGroup) vh.view.findViewById(R.id.control_bar);
+                        mPlayPauseButton = controlBar.getChildAt(1);
                     }
 
                     @Override
@@ -265,6 +278,13 @@
         getHost().notifyPlaybackRowChanged();
     }
 
+    /** Update the focus to play pause button. */
+    public void onPlaybackResume() {
+        if (mPlayPauseButton != null) {
+            mPlayPauseButton.requestFocus();
+        }
+    }
+
     @Nullable
     Boolean hasSecondaryRow() {
         if (mSecondaryActionsAdapter == null) {
@@ -292,6 +312,15 @@
         mTransportControls.pause();
     }
 
+    @Override
+    public void updateProgress() {
+        if (mEnableBuffering) {
+            super.updateProgress();
+            long bufferedTimeMs = System.currentTimeMillis() - mProgramStartTimeMs;
+            mPlaybackControlsRow.setBufferedPosition(bufferedTimeMs);
+        }
+    }
+
     /** Notifies closed caption being enabled/disabled to update related UI. */
     void onSubtitleTrackStateChanged(boolean enabled) {
         mClosedCaptioningAction.setIndex(
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java
index bef036e..81abb8e 100644
--- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java
@@ -39,9 +39,6 @@
 import com.android.tv.util.images.ImageLoader;
 
 class DvrPlaybackMediaSessionHelper {
-    private static final String TAG = "DvrPlaybackMediaSessionHelper";
-    private static final boolean DEBUG = false;
-
     private int mNowPlayingCardWidth;
     private int mNowPlayingCardHeight;
     private int mSpeedLevel;
@@ -73,6 +70,9 @@
                     @Override
                     public void onPlaybackPositionChanged(long positionMs) {
                         updateMediaSessionPlaybackState();
+                        if (getProgram().isPartial()) {
+                            overlayFragment.updateProgress();
+                        }
                         if (mDvrPlayer.isPlaybackPrepared()) {
                             mDvrWatchedPositionManager.setWatchedPosition(
                                     mDvrPlayer.getProgram().getId(), positionMs);
@@ -94,6 +94,11 @@
                             mActivity.startActivity(intent);
                         }
                     }
+
+                    @Override
+                    public void onPlaybackResume() {
+                        overlayFragment.onPlaybackResume();
+                    }
                 });
         initializeMediaSession(mediaSessionTag);
     }
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java
index d3374cf..1059e85 100644
--- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java
@@ -25,7 +25,6 @@
 import android.media.tv.TvContentRating;
 import android.media.tv.TvInputManager;
 import android.media.tv.TvTrackInfo;
-import android.media.tv.TvView;
 import android.os.Bundle;
 import android.support.v17.leanback.app.PlaybackFragment;
 import android.support.v17.leanback.app.PlaybackFragmentGlueHost;
@@ -52,7 +51,7 @@
 import com.android.tv.dvr.ui.SortedArrayAdapter;
 import com.android.tv.dvr.ui.browse.DvrListRowPresenter;
 import com.android.tv.dvr.ui.browse.RecordingCardView;
-import com.android.tv.parental.ContentRatingsManager;
+import com.android.tv.ui.AppLayerTvView;
 import com.android.tv.util.TvSettings;
 import com.android.tv.util.TvTrackInfoUtils;
 import com.android.tv.util.Utils;
@@ -66,6 +65,7 @@
 
     private static final String MEDIA_SESSION_TAG = "com.android.tv.dvr.mediasession";
     private static final float DISPLAY_ASPECT_RATIO_EPSILON = 0.01f;
+    private static final long INVALID_TIME = -1;
 
     // mProgram is only used to store program from intent. Don't use it elsewhere.
     private RecordedProgram mProgram;
@@ -76,8 +76,7 @@
     private SortedArrayAdapter<BaseProgram> mRelatedRecordingsRowAdapter;
     private DvrPlaybackCardPresenter mRelatedRecordingCardPresenter;
     private DvrDataManager mDvrDataManager;
-    private ContentRatingsManager mContentRatingsManager;
-    private TvView mTvView;
+    private AppLayerTvView mTvView;
     private View mBlockScreenView;
     private ListRow mRelatedRecordingsRow;
     private int mVerticalPaddingBase;
@@ -117,10 +116,6 @@
                         .getDimensionPixelOffset(
                                 R.dimen.dvr_playback_overlay_padding_top_no_secondary_row);
         mDvrDataManager = TvSingletons.getSingletons(getActivity()).getDvrDataManager();
-        mContentRatingsManager =
-                TvSingletons.getSingletons(getContext())
-                        .getTvInputManagerHelper()
-                        .getContentRatingsManager();
         if (!mDvrDataManager.isRecordedProgramLoadFinished()) {
             mDvrDataManager.addRecordedProgramLoadFinishedListener(
                     new DvrDataManager.OnRecordedProgramLoadFinishedListener() {
@@ -157,9 +152,9 @@
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
-        mTvView = (TvView) getActivity().findViewById(R.id.dvr_tv_view);
+        mTvView = getActivity().findViewById(R.id.dvr_tv_view);
         mBlockScreenView = getActivity().findViewById(R.id.block_screen);
-        mDvrPlayer = new DvrPlayer(mTvView);
+        mDvrPlayer = new DvrPlayer(mTvView, getActivity());
         mMediaSessionHelper =
                 new DvrPlaybackMediaSessionHelper(
                         getActivity(), MEDIA_SESSION_TAG, mDvrPlayer, this);
@@ -279,6 +274,7 @@
         mPlaybackControlHelper.unregisterCallback();
         mMediaSessionHelper.release();
         mRelatedRecordingCardPresenter.unbindAllViewHolders();
+        mDvrPlayer.release();
         super.onDestroy();
     }
 
@@ -503,6 +499,20 @@
         }
     }
 
+    public void onPlaybackResume() {
+        mPlaybackControlHelper.onPlaybackResume();
+    }
+
+    public long getProgramStartTimeMs() {
+        return (mProgram != null && mProgram.isPartial())
+                ? mProgram.getStartTimeUtcMillis()
+                : INVALID_TIME;
+    }
+
+    public void updateProgress() {
+        mPlaybackControlHelper.updateProgress();
+    }
+
     private class RelatedRecordingsAdapter extends SortedArrayAdapter<BaseProgram> {
         RelatedRecordingsAdapter(DvrPlaybackCardPresenter presenter) {
             super(new SinglePresenterSelector(presenter), BaseProgram.EPISODE_COMPARATOR);
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlayer.java b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java
index 85bb31b..d14646b 100644
--- a/src/com/android/tv/dvr/ui/playback/DvrPlayer.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java
@@ -16,6 +16,7 @@
 
 package com.android.tv.dvr.ui.playback;
 
+import android.content.Context;
 import android.media.PlaybackParams;
 import android.media.session.PlaybackState;
 import android.media.tv.TvContentRating;
@@ -24,12 +25,16 @@
 import android.media.tv.TvView;
 import android.text.TextUtils;
 import android.util.Log;
+import com.android.tv.common.compat.TvViewCompat.TvInputCallbackCompat;
+import com.android.tv.dvr.DvrTvView;
 import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.ui.AppLayerTvView;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 
-class DvrPlayer {
+/** Player for recorded programs. */
+public class DvrPlayer {
     private static final String TAG = "DvrPlayer";
     private static final boolean DEBUG = false;
 
@@ -40,10 +45,11 @@
 
     private static final long SEEK_POSITION_MARGIN_MS = TimeUnit.SECONDS.toMillis(2);
     private static final long REWIND_POSITION_MARGIN_MS = 32; // Workaround value. b/29994826
+    private static final long FORWARD_POSITION_MARGIN_MS = TimeUnit.SECONDS.toMillis(5);
 
     private RecordedProgram mProgram;
     private long mInitialSeekPositionMs;
-    private final TvView mTvView;
+    private final DvrTvView mTvView;
     private DvrPlayerCallback mCallback;
     private OnAspectRatioChangedListener mOnAspectRatioChangedListener;
     private OnContentBlockedListener mOnContentBlockedListener;
@@ -63,6 +69,7 @@
     private long mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
     private boolean mTimeShiftPlayAvailable;
 
+    /** Callback of DVR player. */
     public static class DvrPlayerCallback {
         /**
          * Called when the playback position is changed. The normal updating frequency is around 1
@@ -74,8 +81,11 @@
         public void onPlaybackStateChanged(int playbackState, int playbackSpeed) {}
         /** Called when the playback toward the end. */
         public void onPlaybackEnded() {}
+        /** Called when the playback is resumed to live position. */
+        public void onPlaybackResume() {}
     }
 
+    /** Listener for aspect ratio changed events. */
     public interface OnAspectRatioChangedListener {
         /**
          * Called when the Video's aspect ratio is changed.
@@ -86,27 +96,32 @@
         void onAspectRatioChanged(float videoAspectRatio);
     }
 
+    /** Listener for content blocked events. */
     public interface OnContentBlockedListener {
         /** Called when the Video's aspect ratio is changed. */
         void onContentBlocked(TvContentRating rating);
     }
 
+    /** Listener for tracks availability changed events */
     public interface OnTracksAvailabilityChangedListener {
         /** Called when the Video's subtitle or audio tracks are changed. */
         void onTracksAvailabilityChanged(boolean hasClosedCaption, boolean hasMultiAudio);
     }
 
+    /** Listener for track selected events */
     public interface OnTrackSelectedListener {
         /** Called when certain subtitle or audio track is selected. */
         void onTrackSelected(String selectedTrackId);
     }
 
-    public DvrPlayer(TvView tvView) {
-        mTvView = tvView;
+    /** Constructor of DvrPlayer. */
+    public DvrPlayer(AppLayerTvView tvView, Context context) {
+        mTvView = new DvrTvView(context, tvView, this);
         mTvView.setCaptionEnabled(true);
         mPlaybackParams.setSpeed(1.0f);
         setTvViewCallbacks();
         setCallback(null);
+        mTvView.init();
     }
 
     /**
@@ -333,7 +348,8 @@
 
     /** Returns the audio tracks of the current playback. */
     public ArrayList<TvTrackInfo> getAudioTracks() {
-        return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_AUDIO));
+        List<TvTrackInfo> tracks = mTvView.getTracks(TvTrackInfo.TYPE_AUDIO);
+        return tracks == null ? new ArrayList<>() : new ArrayList<>(tracks);
     }
 
     /** Returns the ID of the selected track of the given type. */
@@ -352,6 +368,10 @@
                 && mPlaybackState != PlaybackState.STATE_CONNECTING;
     }
 
+    public void release() {
+        mTvView.release();
+    }
+
     /**
      * Selects the given track.
      *
@@ -426,9 +446,16 @@
                             resumeToWatchedPositionIfNeeded();
                         }
                         timeMs -= mStartPositionMs;
-                        if (mPlaybackState == PlaybackState.STATE_REWINDING
-                                && timeMs <= REWIND_POSITION_MARGIN_MS) {
+                        long bufferedTimeMs =
+                                System.currentTimeMillis()
+                                        - mProgram.getStartTimeUtcMillis()
+                                        - FORWARD_POSITION_MARGIN_MS;
+                        if ((mPlaybackState == PlaybackState.STATE_REWINDING
+                                        && timeMs <= REWIND_POSITION_MARGIN_MS)
+                                || (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING
+                                        && timeMs > bufferedTimeMs)) {
                             play();
+                            mCallback.onPlaybackResume();
                         } else {
                             mTimeShiftCurrentPositionMs = getRealSeekPosition(timeMs, 0);
                             mCallback.onPlaybackPositionChanged(mTimeShiftCurrentPositionMs);
@@ -440,7 +467,7 @@
                     }
                 });
         mTvView.setCallback(
-                new TvView.TvInputCallback() {
+                new TvInputCallbackCompat() {
                     @Override
                     public void onTimeShiftStatusChanged(String inputId, int status) {
                         if (DEBUG) Log.d(TAG, "onTimeShiftStatusChanged:" + status);
diff --git a/src/com/android/tv/features/PartnerFeatures.java b/src/com/android/tv/features/PartnerFeatures.java
new file mode 100644
index 0000000..6d680b7
--- /dev/null
+++ b/src/com/android/tv/features/PartnerFeatures.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.features;
+
+import android.content.Context;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import com.android.tv.common.feature.Feature;
+import com.google.android.tv.partner.support.PartnerCustomizations;
+
+/** Features backed by {@link PartnerCustomizations}. */
+@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated
+public final class PartnerFeatures {
+
+    public static final Feature TVPROVIDER_ALLOWS_SYSTEM_INSERTS_TO_PROGRAM_TABLE =
+            new PartnerFeature(
+                    PartnerCustomizations.TVPROVIDER_ALLOWS_SYSTEM_INSERTS_TO_PROGRAM_TABLE);
+
+    public static final Feature TURN_OFF_EMBEDDED_TUNER =
+            new PartnerFeature(PartnerCustomizations.TURN_OFF_EMBEDDED_TUNER);
+
+    public static final Feature TVPROVIDER_ALLOWS_COLUMN_CREATION =
+            new PartnerFeature(PartnerCustomizations.TVPROVIDER_ALLOWS_COLUMN_CREATION);
+
+    private static class PartnerFeature implements Feature {
+
+        private final String property;
+
+        public PartnerFeature(String property) {
+            this.property = property;
+        }
+
+        @Override
+        public boolean isEnabled(Context context) {
+            if (VERSION.SDK_INT >= VERSION_CODES.N) {
+                PartnerCustomizations partnerCustomizations = new PartnerCustomizations(context);
+                return partnerCustomizations.getBooleanResource(context, property).orElse(false);
+            }
+            return false;
+        }
+    }
+
+    private PartnerFeatures() {}
+}
diff --git a/src/com/android/tv/TvFeatures.java b/src/com/android/tv/features/TvFeatures.java
similarity index 61%
rename from src/com/android/tv/TvFeatures.java
rename to src/com/android/tv/features/TvFeatures.java
index d2cf76e..208d53f 100644
--- a/src/com/android/tv/TvFeatures.java
+++ b/src/com/android/tv/features/TvFeatures.java
@@ -14,13 +14,15 @@
  * limitations under the License
  */
 
-package com.android.tv;
+package com.android.tv.features;
 
-import static com.android.tv.common.feature.EngOnlyFeature.ENG_ONLY_FEATURE;
-import static com.android.tv.common.feature.FeatureUtils.AND;
+import static com.android.tv.common.feature.BuildTypeFeature.ASOP_FEATURE;
+import static com.android.tv.common.feature.BuildTypeFeature.ENG_ONLY_FEATURE;
 import static com.android.tv.common.feature.FeatureUtils.OFF;
 import static com.android.tv.common.feature.FeatureUtils.ON;
-import static com.android.tv.common.feature.FeatureUtils.OR;
+import static com.android.tv.common.feature.FeatureUtils.and;
+import static com.android.tv.common.feature.FeatureUtils.not;
+import static com.android.tv.common.feature.FeatureUtils.or;
 
 import android.content.Context;
 import android.content.pm.PackageManager;
@@ -31,14 +33,14 @@
 import com.android.tv.common.feature.ExperimentFeature;
 import com.android.tv.common.feature.Feature;
 import com.android.tv.common.feature.FeatureUtils;
-import com.android.tv.common.feature.GServiceFeature;
+import com.android.tv.common.feature.FlagFeature;
 import com.android.tv.common.feature.PropertyFeature;
 import com.android.tv.common.feature.Sdk;
 import com.android.tv.common.feature.TestableFeature;
+import com.android.tv.common.flags.has.HasUiFlags;
+import com.android.tv.common.singletons.HasSingletons;
 import com.android.tv.common.util.PermissionUtils;
 
-import com.google.android.tv.partner.support.PartnerCustomizations;
-
 /**
  * List of {@link Feature} for the Live TV App.
  *
@@ -46,33 +48,48 @@
  */
 public final class TvFeatures extends CommonFeatures {
 
+    /** When enabled store network affiliation information to TV provider */
+    public static final Feature STORE_NETWORK_AFFILIATION = ENG_ONLY_FEATURE;
+
     /** When enabled use system setting for turning on analytics. */
     public static final Feature ANALYTICS_OPT_IN =
             ExperimentFeature.from(Experiments.ENABLE_ANALYTICS_VIA_CHECKBOX);
-    /** When enabled shows a list of failed recordings */
-    public static final Feature DVR_FAILED_LIST = ENG_ONLY_FEATURE;
     /**
      * Analytics that include sensitive information such as channel or program identifiers.
      *
      * <p>See <a href="http://b/22062676">b/22062676</a>
      */
-    public static final Feature ANALYTICS_V2 = AND(ON, ANALYTICS_OPT_IN);
+    public static final Feature ANALYTICS_V2 = and(ON, ANALYTICS_OPT_IN);
 
-    public static final Feature EPG_SEARCH =
-            PropertyFeature.create("feature_tv_use_epg_search", false);
+    private static final Feature TV_PROVIDER_ALLOWS_INSERT_TO_PROGRAM_TABLE =
+            or(Sdk.AT_LEAST_O, PartnerFeatures.TVPROVIDER_ALLOWS_SYSTEM_INSERTS_TO_PROGRAM_TABLE);
 
-    private static final String GSERVICE_KEY_UNHIDE = "live_channels_unhide";
+    /**
+     * Enable cloud EPG for third parties.
+     *
+     * @see <a href="http://go/cloud-epg-3p-proposal">go/cloud-epg-3p-proposal</a>
+     */
+    // TODO verify customization for N
+    public static final TestableFeature CLOUD_EPG_FOR_3RD_PARTY =
+            TestableFeature.createTestableFeature(
+                    and(
+                            not(ASOP_FEATURE),
+                            // TODO(b/66696290): use newer version of robolectric.
+                            or(
+                                    TV_PROVIDER_ALLOWS_INSERT_TO_PROGRAM_TABLE,
+                                    FeatureUtils.ROBOLECTRIC)));
+
+    // TODO(b/76149661): Fix EPG search or remove it
+    public static final Feature EPG_SEARCH = OFF;
+
     /** A flag which indicates that LC app is unhidden even when there is no input. */
     public static final Feature UNHIDE =
-            OR(
-                    new GServiceFeature(GSERVICE_KEY_UNHIDE, false),
-                    new Feature() {
-                        @Override
-                        public boolean isEnabled(Context context) {
-                            // If LC app runs as non-system app, we unhide the app.
-                            return !PermissionUtils.hasAccessAllEpg(context);
-                        }
-                    });
+            or(
+                    FlagFeature.from(
+                            context -> HasSingletons.get(HasUiFlags.class, context),
+                            input -> input.getUiFlags().uhideLauncher()),
+                    // If LC app runs as non-system app, we unhide the app.
+                    not(PermissionUtils::hasAccessAllEpg));
 
     public static final Feature PICTURE_IN_PICTURE =
             new Feature() {
diff --git a/src/com/android/tv/guide/ProgramGuide.java b/src/com/android/tv/guide/ProgramGuide.java
index 5b53f90..bc1b11b 100644
--- a/src/com/android/tv/guide/ProgramGuide.java
+++ b/src/com/android/tv/guide/ProgramGuide.java
@@ -47,7 +47,7 @@
 import com.android.tv.ChannelTuner;
 import com.android.tv.MainActivity;
 import com.android.tv.R;
-import com.android.tv.TvFeatures;
+import com.android.tv.TvSingletons;
 import com.android.tv.analytics.Tracker;
 import com.android.tv.common.WeakHandler;
 import com.android.tv.common.util.DurationTimer;
@@ -56,11 +56,16 @@
 import com.android.tv.data.ProgramDataManager;
 import com.android.tv.dvr.DvrDataManager;
 import com.android.tv.dvr.DvrScheduleManager;
+import com.android.tv.features.TvFeatures;
+import com.android.tv.perf.EventNames;
+import com.android.tv.perf.PerformanceMonitor;
+import com.android.tv.perf.TimerEvent;
 import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter;
 import com.android.tv.ui.ViewUtils;
 import com.android.tv.ui.hideable.AutoHideScheduler;
 import com.android.tv.util.TvInputManagerHelper;
 import com.android.tv.util.Utils;
+import com.android.tv.common.flags.BackendKnobsFlags;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
@@ -150,6 +155,9 @@
 
     private final ProgramManagerListener mProgramManagerListener = new ProgramManagerListener();
 
+    private final PerformanceMonitor mPerformanceMonitor;
+    private TimerEvent mTimerEvent;
+
     private final Runnable mUpdateTimeIndicator =
             new Runnable() {
                 @Override
@@ -175,13 +183,17 @@
             Runnable preShowRunnable,
             Runnable postHideRunnable) {
         mActivity = activity;
+        TvSingletons singletons = TvSingletons.getSingletons(mActivity);
+        mPerformanceMonitor = singletons.getPerformanceMonitor();
+        BackendKnobsFlags backendKnobsFlags = singletons.getBackendKnobs();
         mProgramManager =
                 new ProgramManager(
                         tvInputManagerHelper,
                         channelDataManager,
                         programDataManager,
                         dvrDataManager,
-                        dvrScheduleManager);
+                        dvrScheduleManager,
+                        backendKnobsFlags);
         mChannelTuner = channelTuner;
         mTracker = tracker;
         mPreShowRunnable = preShowRunnable;
@@ -316,12 +328,43 @@
         mGrid.setItemAlignmentOffset(0);
         mGrid.setItemAlignmentOffsetPercent(ProgramGrid.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
 
+        mGrid.addOnScrollListener(
+                new RecyclerView.OnScrollListener() {
+                    @Override
+                    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+                        if (DEBUG) {
+                            Log.d(TAG, "ProgramGrid onScrollStateChanged. newState=" + newState);
+                        }
+                        if (newState == RecyclerView.SCROLL_STATE_SETTLING) {
+                            mPerformanceMonitor.startJankRecorder(
+                                    EventNames.PROGRAM_GUIDE_SCROLL_VERTICALLY);
+                        } else if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+                            mPerformanceMonitor.stopJankRecorder(
+                                    EventNames.PROGRAM_GUIDE_SCROLL_VERTICALLY);
+                        }
+                    }
+                });
+
         RecyclerView.OnScrollListener onScrollListener =
                 new RecyclerView.OnScrollListener() {
                     @Override
                     public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                         onHorizontalScrolled(dx);
                     }
+
+                    @Override
+                    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+                        if (DEBUG) {
+                            Log.d(TAG, "TimelineRow onScrollStateChanged. newState=" + newState);
+                        }
+                        if (newState == RecyclerView.SCROLL_STATE_SETTLING) {
+                            mPerformanceMonitor.startJankRecorder(
+                                    EventNames.PROGRAM_GUIDE_SCROLL_HORIZONTALLY);
+                        } else if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+                            mPerformanceMonitor.stopJankRecorder(
+                                    EventNames.PROGRAM_GUIDE_SCROLL_HORIZONTALLY);
+                        }
+                    }
                 };
         mTimelineRow.addOnScrollListener(onScrollListener);
 
@@ -332,6 +375,18 @@
                         R.animator.program_guide_side_panel_enter_full,
                         0,
                         R.animator.program_guide_table_enter_full);
+        mShowAnimatorFull.addListener(
+                new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        if (mTimerEvent != null) {
+                            mPerformanceMonitor.stopTimer(
+                                    mTimerEvent, EventNames.PROGRAM_GUIDE_SHOW);
+                            mTimerEvent = null;
+                        }
+                        mPerformanceMonitor.stopJankRecorder(EventNames.PROGRAM_GUIDE_SHOW);
+                    }
+                });
 
         mShowAnimatorPartial =
                 createAnimator(
@@ -345,6 +400,16 @@
                         mSidePanelGridView.setVisibility(View.VISIBLE);
                         mSidePanelGridView.setAlpha(1.0f);
                     }
+
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        if (mTimerEvent != null) {
+                            mPerformanceMonitor.stopTimer(
+                                    mTimerEvent, EventNames.PROGRAM_GUIDE_SHOW);
+                            mTimerEvent = null;
+                        }
+                        mPerformanceMonitor.stopJankRecorder(EventNames.PROGRAM_GUIDE_SHOW);
+                    }
                 });
 
         mHideAnimatorFull =
@@ -355,6 +420,11 @@
         mHideAnimatorFull.addListener(
                 new AnimatorListenerAdapter() {
                     @Override
+                    public void onAnimationStart(Animator animation) {
+                        mPerformanceMonitor.recordMemory(EventNames.MEMORY_ON_PROGRAM_GUIDE_CLOSE);
+                    }
+
+                    @Override
                     public void onAnimationEnd(Animator animation) {
                         mContainer.setVisibility(View.GONE);
                     }
@@ -367,6 +437,11 @@
         mHideAnimatorPartial.addListener(
                 new AnimatorListenerAdapter() {
                     @Override
+                    public void onAnimationStart(Animator animation) {
+                        mPerformanceMonitor.recordMemory(EventNames.MEMORY_ON_PROGRAM_GUIDE_CLOSE);
+                    }
+
+                    @Override
                     public void onAnimationEnd(Animator animation) {
                         mContainer.setVisibility(View.GONE);
                     }
@@ -447,6 +522,8 @@
         if (mContainer.getVisibility() == View.VISIBLE) {
             return;
         }
+        mTimerEvent = mPerformanceMonitor.startTimer();
+        mPerformanceMonitor.startJankRecorder(EventNames.PROGRAM_GUIDE_SHOW);
         mTracker.sendShowEpg();
         mTracker.sendScreenView(SCREEN_NAME);
         if (mPreShowRunnable != null) {
@@ -643,6 +720,11 @@
         return mGrid;
     }
 
+    /** Returns if Accessibility is enabled. */
+    boolean isAccessibilityEnabled() {
+        return mAccessibilityManager.isEnabled();
+    }
+
     /** Gets {@link VerticalGridView} for "genre select" side panel. */
     VerticalGridView getSidePanel() {
         return mSidePanelGridView;
@@ -711,9 +793,7 @@
     }
 
     private void startFull() {
-        if (!mShowGuidePartial || mAccessibilityManager.isEnabled()) {
-            // If accessibility service is enabled, focus cannot be moved to side panel due to it's
-            // hidden. Therefore, we don't hide side panel when accessibility service is enabled.
+        if (!mShowGuidePartial) {
             return;
         }
         mShowGuidePartial = false;
@@ -806,13 +886,7 @@
             detailView.setVisibility(View.VISIBLE);
 
             final ProgramRow programRow = (ProgramRow) row.findViewById(R.id.row);
-            programRow.post(
-                    new Runnable() {
-                        @Override
-                        public void run() {
-                            programRow.focusCurrentProgram();
-                        }
-                    });
+            programRow.post(programRow::focusCurrentProgram);
         } else {
             animateRowChange(mSelectedRow, row);
         }
@@ -935,6 +1009,7 @@
         private static final int UNKNOWN = 0;
         private static final int SIDE_PANEL = 1;
         private static final int PROGRAM_TABLE = 2;
+        private static final int CHANNEL_COLUMN = 3;
 
         @Override
         public void onGlobalFocusChanged(View oldFocus, View newFocus) {
@@ -948,6 +1023,10 @@
                 startFull();
             } else if (fromLocation == PROGRAM_TABLE && toLocation == SIDE_PANEL) {
                 startPartial();
+            } else if (fromLocation == CHANNEL_COLUMN && toLocation == PROGRAM_TABLE) {
+                startFull();
+            } else if (fromLocation == PROGRAM_TABLE && toLocation == CHANNEL_COLUMN) {
+                startPartial();
             }
         }
 
@@ -959,7 +1038,11 @@
                 if (obj == mSidePanel) {
                     return SIDE_PANEL;
                 } else if (obj == mGrid) {
-                    return PROGRAM_TABLE;
+                    if (view instanceof ProgramItemView) {
+                        return PROGRAM_TABLE;
+                    } else {
+                        return CHANNEL_COLUMN;
+                    }
                 }
             }
             return UNKNOWN;
diff --git a/src/com/android/tv/guide/ProgramItemView.java b/src/com/android/tv/guide/ProgramItemView.java
index 9f379e4..a46beab 100644
--- a/src/com/android/tv/guide/ProgramItemView.java
+++ b/src/com/android/tv/guide/ProgramItemView.java
@@ -103,12 +103,9 @@
                             tvActivity.getChannelDataManager().getChannel(entry.channelId);
                     if (entry.isCurrentProgram()) {
                         view.postDelayed(
-                                new Runnable() {
-                                    @Override
-                                    public void run() {
-                                        tvActivity.tuneToChannel(channel);
-                                        tvActivity.hideOverlaysForTune();
-                                    }
+                                () -> {
+                                    tvActivity.tuneToChannel(channel);
+                                    tvActivity.hideOverlaysForTune();
                                 },
                                 entry.getWidth() > ((ProgramItemView) view).mMaxWidthForRipple
                                         ? 0
@@ -125,13 +122,9 @@
                                 DvrUiHelper.checkStorageStatusAndShowErrorMessage(
                                         tvActivity,
                                         channel.getInputId(),
-                                        new Runnable() {
-                                            @Override
-                                            public void run() {
+                                        () ->
                                                 DvrUiHelper.requestRecordingFutureProgram(
-                                                        tvActivity, entry.program, false);
-                                            }
-                                        });
+                                                        tvActivity, entry.program, false));
                             } else {
                                 dvrManager.removeScheduledRecording(entry.scheduledRecording);
                                 String msg =
@@ -378,7 +371,7 @@
         int iconResId = 0;
         if (isEntryWideEnough() && mTableEntry.scheduledRecording != null) {
             if (mDvrManager.isConflicting(mTableEntry.scheduledRecording)) {
-                iconResId = R.drawable.ic_warning_white_18dp;
+                iconResId = R.drawable.quantum_ic_warning_white_18;
             } else {
                 switch (mTableEntry.scheduledRecording.getState()) {
                     case ScheduledRecording.STATE_RECORDING_NOT_STARTED:
@@ -405,20 +398,22 @@
         if (channel != null) {
             description = channel.getDisplayNumber() + " " + description;
         }
-        description +=
-                " "
-                        + Utils.getDurationString(
-                                getContext(),
-                                mClock,
-                                mTableEntry.entryStartUtcMillis,
-                                mTableEntry.entryEndUtcMillis,
-                                true);
         Program program = mTableEntry.program;
         if (program != null) {
+            description += " " + program.getDurationString(getContext());
             String episodeDescription = program.getEpisodeContentDescription(getContext());
             if (!TextUtils.isEmpty(episodeDescription)) {
                 description += " " + episodeDescription;
             }
+        } else {
+            description +=
+                    " "
+                            + Utils.getDurationString(
+                                    getContext(),
+                                    mClock,
+                                    mTableEntry.entryStartUtcMillis,
+                                    mTableEntry.entryEndUtcMillis,
+                                    true);
         }
         if (mTableEntry.scheduledRecording != null) {
             if (mDvrManager.isConflicting(mTableEntry.scheduledRecording)) {
diff --git a/src/com/android/tv/guide/ProgramManager.java b/src/com/android/tv/guide/ProgramManager.java
index 3f20a83..3a5a4a0 100644
--- a/src/com/android/tv/guide/ProgramManager.java
+++ b/src/com/android/tv/guide/ProgramManager.java
@@ -32,6 +32,7 @@
 import com.android.tv.dvr.data.ScheduledRecording;
 import com.android.tv.util.TvInputManagerHelper;
 import com.android.tv.util.Utils;
+import com.android.tv.common.flags.BackendKnobsFlags;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -59,6 +60,7 @@
     private final ProgramDataManager mProgramDataManager;
     private final DvrDataManager mDvrDataManager; // Only set if DVR is enabled
     private final DvrScheduleManager mDvrScheduleManager;
+    private final BackendKnobsFlags mBackendKnobsFlags;
 
     private long mStartUtcMillis;
     private long mEndUtcMillis;
@@ -114,12 +116,26 @@
                 }
             };
 
-    private final ProgramDataManager.Listener mProgramDataManagerListener =
-            new ProgramDataManager.Listener() {
+    private final ProgramDataManager.Callback mProgramDataManagerCallback =
+            new ProgramDataManager.Callback() {
                 @Override
                 public void onProgramUpdated() {
                     updateTableEntries(true);
                 }
+
+                @Override
+                public void onSingleChannelUpdated(long channelId) {
+                    boolean parentalControlsEnabled =
+                            mTvInputManagerHelper
+                                    .getParentalControlSettings()
+                                    .isParentalControlsEnabled();
+                    // Inline the updating of the mChannelIdEntriesMap here so we can only call
+                    // getParentalControlSettings once.
+                    List<TableEntry> entries =
+                            createProgramEntries(channelId, parentalControlsEnabled);
+                    mChannelIdEntriesMap.put(channelId, entries);
+                    notifyTableEntriesUpdated();
+                }
             };
 
     private final DvrDataManager.ScheduledRecordingListener mScheduledRecordingListener =
@@ -199,19 +215,21 @@
             ChannelDataManager channelDataManager,
             ProgramDataManager programDataManager,
             @Nullable DvrDataManager dvrDataManager,
-            @Nullable DvrScheduleManager dvrScheduleManager) {
+            @Nullable DvrScheduleManager dvrScheduleManager,
+            BackendKnobsFlags backendKnobsFlags) {
         mTvInputManagerHelper = tvInputManagerHelper;
         mChannelDataManager = channelDataManager;
         mProgramDataManager = programDataManager;
         mDvrDataManager = dvrDataManager;
         mDvrScheduleManager = dvrScheduleManager;
+        mBackendKnobsFlags = backendKnobsFlags;
     }
 
     void programGuideVisibilityChanged(boolean visible) {
         mProgramDataManager.setPauseProgramUpdate(visible);
         if (visible) {
             mChannelDataManager.addListener(mChannelDataManagerListener);
-            mProgramDataManager.addListener(mProgramDataManagerListener);
+            mProgramDataManager.addCallback(mProgramDataManagerCallback);
             if (mDvrDataManager != null) {
                 if (!mDvrDataManager.isDvrScheduleLoadFinished()) {
                     mDvrDataManager.addDvrScheduleLoadFinishedListener(mDvrLoadedListener);
@@ -224,7 +242,7 @@
             }
         } else {
             mChannelDataManager.removeListener(mChannelDataManagerListener);
-            mProgramDataManager.removeListener(mProgramDataManagerListener);
+            mProgramDataManager.removeCallback(mProgramDataManagerCallback);
             if (mDvrDataManager != null) {
                 mDvrDataManager.removeDvrScheduleLoadFinishedListener(mDvrLoadedListener);
                 mDvrDataManager.removeScheduledRecordingListener(mScheduledRecordingListener);
@@ -233,6 +251,7 @@
                 mDvrScheduleManager.removeOnConflictStateChangeListener(
                         mOnConflictStateChangeListener);
             }
+            mChannelIdEntriesMap.clear();
         }
     }
 
@@ -309,8 +328,8 @@
         long fromUtcMillis = mFromUtcMillis + timeMillisToScroll;
         long toUtcMillis = mToUtcMillis + timeMillisToScroll;
         if (fromUtcMillis < mStartUtcMillis) {
-            fromUtcMillis = mStartUtcMillis;
             toUtcMillis += mStartUtcMillis - fromUtcMillis;
+            fromUtcMillis = mStartUtcMillis;
         }
         if (toUtcMillis > mEndUtcMillis) {
             fromUtcMillis -= toUtcMillis - mEndUtcMillis;
@@ -345,10 +364,12 @@
     /** Returns the program index of the program at {@code time} or -1 if not found. */
     int getProgramIndexAtTime(long channelId, long time) {
         List<TableEntry> entries = mChannelIdEntriesMap.get(channelId);
-        for (int i = 0; i < entries.size(); ++i) {
-            TableEntry entry = entries.get(i);
-            if (entry.entryStartUtcMillis <= time && time < entry.entryEndUtcMillis) {
-                return i;
+        if (entries != null) {
+            for (int i = 0; i < entries.size(); ++i) {
+                TableEntry entry = entries.get(i);
+                if (entry.entryStartUtcMillis <= time && time < entry.entryEndUtcMillis) {
+                    return i;
+                }
             }
         }
         return -1;
@@ -401,7 +422,7 @@
      * given {@code channelId}.
      */
     int getTableEntryCount(long channelId) {
-        return mChannelIdEntriesMap.get(channelId).size();
+        return mChannelIdEntriesMap.isEmpty() ? 0 : mChannelIdEntriesMap.get(channelId).size();
     }
 
     /**
@@ -410,6 +431,9 @@
      * (e.g., whose channelId is INVALID_ID), when it corresponds to a gap between programs.
      */
     TableEntry getTableEntry(long channelId, int index) {
+        if (mBackendKnobsFlags.enablePartialProgramFetch()) {
+            mProgramDataManager.prefetchChannel(channelId);
+        }
         return mChannelIdEntriesMap.get(channelId).get(index);
     }
 
@@ -437,6 +461,14 @@
         buildGenreFilters();
     }
 
+    /** Sets the channel list for testing */
+    void setChannels(List<Channel> channels) {
+        mChannels = new ArrayList<>(channels);
+        mSelectedGenreId = GenreItems.ID_ALL_CHANNELS;
+        mFilteredChannels = mChannels;
+        buildGenreFilters();
+    }
+
     private void updateTableEntries(boolean clear) {
         updateTableEntriesWithoutNotification(clear);
         notifyTableEntriesUpdated();
@@ -544,6 +576,9 @@
 
     @Nullable
     private TableEntry getTableEntry(long channelId, long entryId) {
+        if (mChannelIdEntriesMap.isEmpty()) {
+            return null;
+        }
         List<TableEntry> entries = mChannelIdEntriesMap.get(channelId);
         if (entries != null) {
             for (TableEntry entry : entries) {
diff --git a/src/com/android/tv/guide/ProgramRow.java b/src/com/android/tv/guide/ProgramRow.java
index 83175bb..3317c15 100644
--- a/src/com/android/tv/guide/ProgramRow.java
+++ b/src/com/android/tv/guide/ProgramRow.java
@@ -72,6 +72,9 @@
 
     public ProgramRow(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
+        ProgramRowAccessibilityDelegate rowAccessibilityDelegate =
+                new ProgramRowAccessibilityDelegate(this);
+        this.setAccessibilityDelegateCompat(rowAccessibilityDelegate);
     }
 
     /** Registers a listener focus events occurring on children to the {@code ProgramRow}. */
@@ -126,13 +129,26 @@
                 : direction == View.FOCUS_LEFT;
     }
 
+    // When Accessibility is enabled, this API will keep next node visible
+    void focusSearchAccessibility(View focused, int direction) {
+        TableEntry focusedEntry = ((ProgramItemView) focused).getTableEntry();
+        long toMillis = mProgramManager.getToUtcMillis();
+
+        if (isDirectionEnd(direction) || direction == View.FOCUS_FORWARD) {
+            if (focusedEntry.entryEndUtcMillis >= toMillis) {
+                scrollByTime(focusedEntry.entryEndUtcMillis - toMillis + HALF_HOUR_MILLIS);
+            }
+        }
+    }
+
     @Override
     public View focusSearch(View focused, int direction) {
         TableEntry focusedEntry = ((ProgramItemView) focused).getTableEntry();
         long fromMillis = mProgramManager.getFromUtcMillis();
         long toMillis = mProgramManager.getToUtcMillis();
 
-        if (isDirectionStart(direction) || direction == View.FOCUS_BACKWARD) {
+        if (!mProgramGuide.isAccessibilityEnabled()
+                && (isDirectionStart(direction) || direction == View.FOCUS_BACKWARD)) {
             if (focusedEntry.entryStartUtcMillis < fromMillis) {
                 // The current entry starts outside of the view; Align or scroll to the left.
                 scrollByTime(
@@ -162,7 +178,9 @@
         TableEntry targetEntry = ((ProgramItemView) target).getTableEntry();
 
         if (isDirectionStart(direction) || direction == View.FOCUS_BACKWARD) {
-            if (targetEntry.entryStartUtcMillis < fromMillis
+            if (mProgramGuide.isAccessibilityEnabled()) {
+                scrollByTime(targetEntry.entryStartUtcMillis - fromMillis);
+            } else if (targetEntry.entryStartUtcMillis < fromMillis
                     && targetEntry.entryEndUtcMillis < fromMillis + HALF_HOUR_MILLIS) {
                 // The target entry starts outside the view; Align or scroll to the left.
                 scrollByTime(
diff --git a/src/com/android/tv/guide/ProgramRowAccessibilityDelegate.java b/src/com/android/tv/guide/ProgramRowAccessibilityDelegate.java
new file mode 100644
index 0000000..5e498be
--- /dev/null
+++ b/src/com/android/tv/guide/ProgramRowAccessibilityDelegate.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.guide;
+
+import android.os.Bundle;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerViewAccessibilityDelegate;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/** AccessibilityDelegate for {@link ProgramRow} */
+class ProgramRowAccessibilityDelegate extends RecyclerViewAccessibilityDelegate {
+    private final ItemDelegate mItemDelegate;
+
+    ProgramRowAccessibilityDelegate(RecyclerView recyclerView) {
+        super(recyclerView);
+
+        mItemDelegate =
+                new ItemDelegate(this) {
+                    @Override
+                    public boolean performAccessibilityAction(View host, int action, Bundle args) {
+                        // Prevent Accessibility service to move the Program Row elements
+                        // Ignoring Accessibility action above Set Text
+                        // (accessibilityActionShowOnScreen)
+                        if (action > AccessibilityNodeInfo.ACTION_SET_TEXT) {
+                            return false;
+                        }
+
+                        return super.performAccessibilityAction(host, action, args);
+                    }
+                };
+    }
+
+    @Override
+    public ItemDelegate getItemDelegate() {
+        return mItemDelegate;
+    }
+
+    @Override
+    public boolean onRequestSendAccessibilityEvent(
+            ViewGroup host, View child, AccessibilityEvent event) {
+        // Forcing the next item to be visible for scrolling in forward direction
+        if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED) {
+            ((ProgramRow) host).focusSearchAccessibility(child, View.FOCUS_FORWARD);
+        }
+        return super.onRequestSendAccessibilityEvent(host, child, event);
+    }
+}
diff --git a/src/com/android/tv/guide/ProgramTableAdapter.java b/src/com/android/tv/guide/ProgramTableAdapter.java
index 6e7485a..7576bf5 100644
--- a/src/com/android/tv/guide/ProgramTableAdapter.java
+++ b/src/com/android/tv/guide/ProgramTableAdapter.java
@@ -110,6 +110,8 @@
     private final int mDvrPaddingStartWithTrack;
     private final int mDvrPaddingStartWithOutTrack;
 
+    private RecyclerView mRecyclerView;
+
     ProgramTableAdapter(Context context, ProgramGuide programGuide) {
         mContext = context;
         mAccessibilityManager =
@@ -198,7 +200,15 @@
             mProgramManager.addTableEntriesUpdatedListener(listAdapter);
             mProgramListAdapters.add(listAdapter);
         }
-        notifyDataSetChanged();
+        if (mRecyclerView != null && mRecyclerView.isComputingLayout()) {
+            // it means that RecyclerView is in a lockdown state and any attempt to update adapter
+            // contents will result in an exception because adapter contents cannot be changed while
+            // RecyclerView is trying to compute the layout
+            // postpone the change using a Handler
+            mHandler.post(this::notifyDataSetChanged);
+        } else {
+            notifyDataSetChanged();
+        }
     }
 
     @Override
@@ -238,8 +248,22 @@
         int channelIndex = mProgramManager.getChannelIndex(tableEntry.channelId);
         int pos = mProgramManager.getProgramIdIndex(tableEntry.channelId, tableEntry.getId());
         if (DEBUG) Log.d(TAG, "update(" + channelIndex + ", " + pos + ")");
-        mProgramListAdapters.get(channelIndex).notifyItemChanged(pos, tableEntry);
-        notifyItemChanged(channelIndex, true);
+        if (channelIndex >= 0 && channelIndex < mProgramListAdapters.size()) {
+            mProgramListAdapters.get(channelIndex).notifyItemChanged(pos, tableEntry);
+            notifyItemChanged(channelIndex, true);
+        }
+    }
+
+    @Override
+    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+        mRecyclerView = recyclerView;
+        super.onAttachedToRecyclerView(recyclerView);
+    }
+
+    @Override
+    public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
+        super.onDetachedFromRecyclerView(recyclerView);
+        mRecyclerView = null;
     }
 
     class ProgramRowViewHolder extends RecyclerView.ViewHolder
@@ -260,13 +284,7 @@
                         }
                     }
                 };
-        private final Runnable mUpdateDetailViewRunnable =
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        updateDetailView();
-                    }
-                };
+        private final Runnable mUpdateDetailViewRunnable = this::updateDetailView;
 
         private final RecyclerView.OnScrollListener mOnScrollListener =
                 new RecyclerView.OnScrollListener() {
@@ -420,12 +438,14 @@
                 mChannelNumberView.setText(displayNumber);
                 mChannelNumberView.setVisibility(View.VISIBLE);
             }
+
+            boolean isChannelLocked = isChannelLocked(channel);
             mChannelNumberView.setTextColor(
-                    isChannelLocked(channel) ? mChannelBlockedTextColor : mChannelTextColor);
+                    isChannelLocked ? mChannelBlockedTextColor : mChannelTextColor);
 
             mChannelLogoView.setImageBitmap(null);
             mChannelLogoView.setVisibility(View.GONE);
-            if (isChannelLocked(channel)) {
+            if (isChannelLocked) {
                 mChannelNameView.setVisibility(View.GONE);
                 mChannelBlockView.setVisibility(View.VISIBLE);
             } else {
@@ -573,13 +593,7 @@
                     mTitleView.setText(text);
                 }
 
-                updateTextView(
-                        mTimeView,
-                        Utils.getDurationString(
-                                context,
-                                program.getStartTimeUtcMillis(),
-                                program.getEndTimeUtcMillis(),
-                                false));
+                updateTextView(mTimeView, program.getDurationString(context));
 
                 boolean trackMetaDataVisible =
                         updateTextView(
diff --git a/src/com/android/tv/menu/ChannelsRowAdapter.java b/src/com/android/tv/menu/ChannelsRowAdapter.java
index 8536ef1..4a9e476 100644
--- a/src/com/android/tv/menu/ChannelsRowAdapter.java
+++ b/src/com/android/tv/menu/ChannelsRowAdapter.java
@@ -20,6 +20,9 @@
 import android.content.Intent;
 import android.media.tv.TvInputInfo;
 import android.view.View;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
+import com.android.tv.ChannelChanger;
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
 import com.android.tv.analytics.Tracker;
@@ -34,9 +37,8 @@
 import java.util.List;
 
 /** An adapter of the Channels row. */
-public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<ChannelsRowItem> {
-    // There are four special cards: guide, setup, dvr, applink.
-    private static final int SIZE_OF_VIEW_TYPE = 5;
+public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<ChannelsRowItem>
+        implements AccessibilityStateChangeListener {
 
     private final Context mContext;
     private final Tracker mTracker;
@@ -44,58 +46,9 @@
     private final DvrDataManager mDvrDataManager;
     private final int mMaxCount;
     private final int mMinCount;
+    private final ChannelChanger mChannelChanger;
 
-    private final View.OnClickListener mGuideOnClickListener =
-            new View.OnClickListener() {
-                @Override
-                public void onClick(View view) {
-                    mTracker.sendMenuClicked(R.string.channels_item_program_guide);
-                    getMainActivity().getOverlayManager().showProgramGuide();
-                }
-            };
-
-    private final View.OnClickListener mSetupOnClickListener =
-            new View.OnClickListener() {
-                @Override
-                public void onClick(View view) {
-                    mTracker.sendMenuClicked(R.string.channels_item_setup);
-                    getMainActivity().getOverlayManager().showSetupFragment();
-                }
-            };
-
-    private final View.OnClickListener mDvrOnClickListener =
-            new View.OnClickListener() {
-                @Override
-                public void onClick(View view) {
-                    mTracker.sendMenuClicked(R.string.channels_item_dvr);
-                    getMainActivity().getOverlayManager().showDvrManager();
-                }
-            };
-
-    private final View.OnClickListener mAppLinkOnClickListener =
-            new View.OnClickListener() {
-                @Override
-                public void onClick(View view) {
-                    mTracker.sendMenuClicked(R.string.channels_item_app_link);
-                    Intent intent = ((AppLinkCardView) view).getIntent();
-                    if (intent != null) {
-                        getMainActivity().startActivitySafe(intent);
-                    }
-                }
-            };
-
-    private final View.OnClickListener mChannelOnClickListener =
-            new View.OnClickListener() {
-                @Override
-                public void onClick(View view) {
-                    // Always send the label "Channels" because the channel ID or name or number
-                    // might be
-                    // sensitive.
-                    mTracker.sendMenuClicked(R.string.menu_title_channels);
-                    getMainActivity().tuneToChannel((Channel) view.getTag());
-                    getMainActivity().hideOverlaysForTune();
-                }
-            };
+    private boolean mShowChannelUpDown;
 
     public ChannelsRowAdapter(
             Context context, Recommender recommender, int minCount, int maxCount) {
@@ -112,6 +65,11 @@
         mMinCount = minCount;
         mMaxCount = maxCount;
         setHasStableIds(true);
+        mChannelChanger = (ChannelChanger) (context);
+        AccessibilityManager accessibilityManager =
+                context.getSystemService(AccessibilityManager.class);
+        mShowChannelUpDown = accessibilityManager.isEnabled();
+        accessibilityManager.addAccessibilityStateChangeListener(this);
     }
 
     @Override
@@ -133,18 +91,22 @@
     public void onBindViewHolder(MyViewHolder viewHolder, int position) {
         int viewType = getItemViewType(position);
         if (viewType == R.layout.menu_card_guide) {
-            viewHolder.itemView.setOnClickListener(mGuideOnClickListener);
+            viewHolder.itemView.setOnClickListener(this::onGuideClicked);
+        } else if (viewType == R.layout.menu_card_up) {
+            viewHolder.itemView.setOnClickListener(this::onChannelUpClicked);
+        } else if (viewType == R.layout.menu_card_down) {
+            viewHolder.itemView.setOnClickListener(this::onChannelDownClicked);
         } else if (viewType == R.layout.menu_card_setup) {
-            viewHolder.itemView.setOnClickListener(mSetupOnClickListener);
+            viewHolder.itemView.setOnClickListener(this::onSetupClicked);
         } else if (viewType == R.layout.menu_card_app_link) {
-            viewHolder.itemView.setOnClickListener(mAppLinkOnClickListener);
+            viewHolder.itemView.setOnClickListener(this::onAppLinkClicked);
         } else if (viewType == R.layout.menu_card_dvr) {
-            viewHolder.itemView.setOnClickListener(mDvrOnClickListener);
+            viewHolder.itemView.setOnClickListener(this::onDvrClicked);
             SimpleCardView view = (SimpleCardView) viewHolder.itemView;
             view.setText(R.string.channels_item_dvr);
         } else {
             viewHolder.itemView.setTag(getItemList().get(position).getChannel());
-            viewHolder.itemView.setOnClickListener(mChannelOnClickListener);
+            viewHolder.itemView.setOnClickListener(this::onChannelClicked);
         }
         super.onBindViewHolder(viewHolder, position);
     }
@@ -158,9 +120,53 @@
         }
     }
 
+    private void onGuideClicked(View unused) {
+        mTracker.sendMenuClicked(R.string.channels_item_program_guide);
+        getMainActivity().getOverlayManager().showProgramGuide();
+    }
+
+    private void onChannelDownClicked(View unused) {
+        mChannelChanger.channelDown();
+    }
+
+    private void onChannelUpClicked(View unused) {
+        mChannelChanger.channelUp();
+    }
+
+    private void onSetupClicked(View unused) {
+        mTracker.sendMenuClicked(R.string.channels_item_setup);
+        getMainActivity().getOverlayManager().showSetupFragment();
+    }
+
+    private void onDvrClicked(View unused) {
+        mTracker.sendMenuClicked(R.string.channels_item_dvr);
+        getMainActivity().getOverlayManager().showDvrManager();
+    }
+
+    private void onAppLinkClicked(View view) {
+        mTracker.sendMenuClicked(R.string.channels_item_app_link);
+        Intent intent = ((AppLinkCardView) view).getIntent();
+        if (intent != null) {
+            getMainActivity().startActivitySafe(intent);
+        }
+    }
+
+    private void onChannelClicked(View view) {
+        // Always send the label "Channels" because the channel ID or name or number might be
+        // sensitive.
+        mTracker.sendMenuClicked(R.string.menu_title_channels);
+        getMainActivity().tuneToChannel((Channel) view.getTag());
+        getMainActivity().hideOverlaysForTune();
+    }
+
     private void createItems() {
         List<ChannelsRowItem> items = new ArrayList<>();
         items.add(ChannelsRowItem.GUIDE_ITEM);
+        if (mShowChannelUpDown) {
+            items.add(ChannelsRowItem.UP_ITEM);
+            items.add(ChannelsRowItem.DOWN_ITEM);
+        }
+
         if (needToShowSetupItem()) {
             items.add(ChannelsRowItem.SETUP_ITEM);
         }
@@ -183,6 +189,12 @@
         // The current index of the item list to iterate. It starts from 1 because the first item
         // (GUIDE) is always visible and not updated.
         int currentIndex = 1;
+        if (updateItem(mShowChannelUpDown, ChannelsRowItem.UP_ITEM, currentIndex)) {
+            ++currentIndex;
+        }
+        if (updateItem(mShowChannelUpDown, ChannelsRowItem.DOWN_ITEM, currentIndex)) {
+            ++currentIndex;
+        }
         if (updateItem(needToShowSetupItem(), ChannelsRowItem.SETUP_ITEM, currentIndex)) {
             ++currentIndex;
         }
@@ -298,4 +310,10 @@
         channelList.add(channel);
         return true;
     }
+
+    @Override
+    public void onAccessibilityStateChanged(boolean enabled) {
+        mShowChannelUpDown = enabled;
+        update();
+    }
 }
diff --git a/src/com/android/tv/menu/ChannelsRowItem.java b/src/com/android/tv/menu/ChannelsRowItem.java
index 608bb36..12976ef 100644
--- a/src/com/android/tv/menu/ChannelsRowItem.java
+++ b/src/com/android/tv/menu/ChannelsRowItem.java
@@ -30,6 +30,10 @@
     public static final int DVR_ITEM_ID = -3;
     /** The item ID for app link item */
     public static final int APP_LINK_ITEM_ID = -4;
+    /** The item ID for channel up item */
+    public static final int UP_ID = -5;
+    /** The item ID for app link item */
+    public static final int DOWN_ID = -6;
 
     /** The item which represents the guide. */
     public static final ChannelsRowItem GUIDE_ITEM =
@@ -44,6 +48,12 @@
     public static final ChannelsRowItem APP_LINK_ITEM =
             new ChannelsRowItem(APP_LINK_ITEM_ID, R.layout.menu_card_app_link);
 
+    /** The item which represents the channel up. */
+    public static final ChannelsRowItem UP_ITEM = new ChannelsRowItem(UP_ID, R.layout.menu_card_up);
+    /** The item which represents the channel down. */
+    public static final ChannelsRowItem DOWN_ITEM =
+            new ChannelsRowItem(DOWN_ID, R.layout.menu_card_down);
+
     private final long mItemId;
     @NonNull private Channel mChannel;
     private final int mLayoutId;
diff --git a/src/com/android/tv/menu/Menu.java b/src/com/android/tv/menu/Menu.java
index 19a93db..6bdbf87 100644
--- a/src/com/android/tv/menu/Menu.java
+++ b/src/com/android/tv/menu/Menu.java
@@ -213,12 +213,9 @@
                 rowIdToSelect,
                 mAnimationDisabledForTest
                         ? null
-                        : new Runnable() {
-                            @Override
-                            public void run() {
-                                if (isActive()) {
-                                    mShowAnimator.start();
-                                }
+                        : () -> {
+                            if (isActive()) {
+                                mShowAnimator.start();
                             }
                         });
         scheduleHide();
diff --git a/src/com/android/tv/menu/MenuAction.java b/src/com/android/tv/menu/MenuAction.java
index 5237253..8c180ca 100644
--- a/src/com/android/tv/menu/MenuAction.java
+++ b/src/com/android/tv/menu/MenuAction.java
@@ -50,12 +50,12 @@
             new MenuAction(
                     R.string.options_item_more_channels,
                     TvOptionsManager.OPTION_MORE_CHANNELS,
-                    R.drawable.ic_store);
+                    R.drawable.ic_app_store);
     public static final MenuAction DEV_ACTION =
             new MenuAction(
                     R.string.options_item_developer,
                     TvOptionsManager.OPTION_DEVELOPER,
-                    R.drawable.ic_developer_mode_tv_white_48dp);
+                    R.drawable.quantum_ic_developer_mode_tv_white_48);
     public static final MenuAction SETTINGS_ACTION =
             new MenuAction(
                     R.string.options_item_settings,
diff --git a/src/com/android/tv/menu/OptionsRowAdapter.java b/src/com/android/tv/menu/OptionsRowAdapter.java
index ceffe86..4e69a60 100644
--- a/src/com/android/tv/menu/OptionsRowAdapter.java
+++ b/src/com/android/tv/menu/OptionsRowAdapter.java
@@ -37,17 +37,14 @@
                 public void onClick(View view) {
                     final MenuAction action = (MenuAction) view.getTag();
                     view.post(
-                            new Runnable() {
-                                @Override
-                                public void run() {
-                                    int resId = action.getActionNameResId();
-                                    if (resId == 0) {
-                                        mTracker.sendMenuClicked(CUSTOM_ACTION_LABEL);
-                                    } else {
-                                        mTracker.sendMenuClicked(resId);
-                                    }
-                                    executeAction(action.getType());
+                            () -> {
+                                int resId = action.getActionNameResId();
+                                if (resId == 0) {
+                                    mTracker.sendMenuClicked(CUSTOM_ACTION_LABEL);
+                                } else {
+                                    mTracker.sendMenuClicked(resId);
                                 }
+                                executeAction(action.getType());
                             });
                 }
             };
diff --git a/src/com/android/tv/menu/PlayControlsRowView.java b/src/com/android/tv/menu/PlayControlsRowView.java
index 496d196..0ce74ae 100644
--- a/src/com/android/tv/menu/PlayControlsRowView.java
+++ b/src/com/android/tv/menu/PlayControlsRowView.java
@@ -185,13 +185,10 @@
                 R.drawable.lb_ic_skip_previous,
                 R.string.play_controls_description_skip_previous,
                 null,
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        if (mTimeShiftManager.isAvailable()) {
-                            mTimeShiftManager.jumpToPrevious();
-                            updateControls(true);
-                        }
+                () -> {
+                    if (mTimeShiftManager.isAvailable()) {
+                        mTimeShiftManager.jumpToPrevious();
+                        updateControls(true);
                     }
                 });
         initializeButton(
@@ -199,13 +196,10 @@
                 R.drawable.lb_ic_fast_rewind,
                 R.string.play_controls_description_fast_rewind,
                 null,
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        if (mTimeShiftManager.isAvailable()) {
-                            mTimeShiftManager.rewind();
-                            updateButtons();
-                        }
+                () -> {
+                    if (mTimeShiftManager.isAvailable()) {
+                        mTimeShiftManager.rewind();
+                        updateButtons();
                     }
                 });
         initializeButton(
@@ -213,13 +207,10 @@
                 R.drawable.lb_ic_play,
                 R.string.play_controls_description_play_pause,
                 null,
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        if (mTimeShiftManager.isAvailable()) {
-                            mTimeShiftManager.togglePlayPause();
-                            updateButtons();
-                        }
+                () -> {
+                    if (mTimeShiftManager.isAvailable()) {
+                        mTimeShiftManager.togglePlayPause();
+                        updateButtons();
                     }
                 });
         initializeButton(
@@ -227,13 +218,10 @@
                 R.drawable.lb_ic_fast_forward,
                 R.string.play_controls_description_fast_forward,
                 null,
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        if (mTimeShiftManager.isAvailable()) {
-                            mTimeShiftManager.fastForward();
-                            updateButtons();
-                        }
+                () -> {
+                    if (mTimeShiftManager.isAvailable()) {
+                        mTimeShiftManager.fastForward();
+                        updateButtons();
                     }
                 });
         initializeButton(
@@ -241,13 +229,10 @@
                 R.drawable.lb_ic_skip_next,
                 R.string.play_controls_description_skip_next,
                 null,
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        if (mTimeShiftManager.isAvailable()) {
-                            mTimeShiftManager.jumpToNext();
-                            updateControls(true);
-                        }
+                () -> {
+                    if (mTimeShiftManager.isAvailable()) {
+                        mTimeShiftManager.jumpToNext();
+                        updateControls(true);
                     }
                 });
         int color =
@@ -257,12 +242,7 @@
                 R.drawable.ic_record_start,
                 R.string.channels_item_record_start,
                 color,
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        onRecordButtonClicked();
-                    }
-                });
+                this::onRecordButtonClicked);
     }
 
     private boolean isCurrentChannelRecording() {
@@ -296,13 +276,9 @@
                 DvrUiHelper.checkStorageStatusAndShowErrorMessage(
                         mMainActivity,
                         currentChannel.getInputId(),
-                        new Runnable() {
-                            @Override
-                            public void run() {
+                        () ->
                                 DvrUiHelper.requestRecordingCurrentProgram(
-                                        mMainActivity, currentChannel, program, true);
-                            }
-                        });
+                                        mMainActivity, currentChannel, program, true));
             }
         } else if (currentChannel != null) {
             DvrUiHelper.showStopRecordingDialog(
@@ -490,15 +466,12 @@
         // After the focus is actually changed, hideRippleAnimation should run
         // to reflect the result of the focus change. To be sure, hideRippleAnimation is posted.
         post(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        mJumpPreviousButton.hideRippleAnimation();
-                        mRewindButton.hideRippleAnimation();
-                        mPlayPauseButton.hideRippleAnimation();
-                        mFastForwardButton.hideRippleAnimation();
-                        mJumpNextButton.hideRippleAnimation();
-                    }
+                () -> {
+                    mJumpPreviousButton.hideRippleAnimation();
+                    mRewindButton.hideRippleAnimation();
+                    mPlayPauseButton.hideRippleAnimation();
+                    mFastForwardButton.hideRippleAnimation();
+                    mJumpNextButton.hideRippleAnimation();
                 });
     }
 
diff --git a/src/com/android/tv/menu/TvOptionsRowAdapter.java b/src/com/android/tv/menu/TvOptionsRowAdapter.java
index 55affb5..fe52b25 100644
--- a/src/com/android/tv/menu/TvOptionsRowAdapter.java
+++ b/src/com/android/tv/menu/TvOptionsRowAdapter.java
@@ -18,12 +18,11 @@
 
 import android.content.Context;
 import android.media.tv.TvTrackInfo;
-import android.support.annotation.VisibleForTesting;
-import com.android.tv.TvFeatures;
 import com.android.tv.TvOptionsManager;
 import com.android.tv.common.customization.CustomAction;
 import com.android.tv.common.util.CommonUtils;
 import com.android.tv.data.DisplayMode;
+import com.android.tv.features.TvFeatures;
 import com.android.tv.ui.TvViewUiManager;
 import com.android.tv.ui.sidepanel.ClosedCaptionFragment;
 import com.android.tv.ui.sidepanel.DeveloperOptionFragment;
@@ -78,7 +77,6 @@
         }
     }
 
-    @VisibleForTesting
     private boolean updateClosedCaptionAction() {
         return updateActionDescription(MenuAction.SELECT_CLOSED_CAPTION_ACTION);
     }
diff --git a/src/com/android/tv/modules/TvApplicationModule.java b/src/com/android/tv/modules/TvApplicationModule.java
new file mode 100644
index 0000000..45383ae
--- /dev/null
+++ b/src/com/android/tv/modules/TvApplicationModule.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.modules;
+
+import android.content.Context;
+import com.android.tv.MainActivity;
+import com.android.tv.TvApplication;
+import com.android.tv.common.concurrent.NamedThreadFactory;
+import com.android.tv.common.dagger.ApplicationModule;
+import com.android.tv.common.dagger.annotations.ApplicationContext;
+import com.android.tv.onboarding.OnboardingActivity;
+import com.android.tv.util.AsyncDbTask;
+import com.android.tv.util.TvInputManagerHelper;
+import dagger.Module;
+import dagger.Provides;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import javax.inject.Singleton;
+
+/** Dagger module for {@link TvApplication}. */
+@Module(
+        includes = {
+            ApplicationModule.class,
+            TvSingletonsModule.class,
+            MainActivity.Module.class,
+            OnboardingActivity.Module.class
+        })
+public class TvApplicationModule {
+    private static final NamedThreadFactory THREAD_FACTORY = new NamedThreadFactory("tv-app-db");
+
+    @Provides
+    @AsyncDbTask.DbExecutor
+    @Singleton
+    Executor providesDbExecutor() {
+        return Executors.newSingleThreadExecutor(THREAD_FACTORY);
+    }
+
+    @Provides
+    @Singleton
+    TvInputManagerHelper providesTvInputManagerHelper(@ApplicationContext Context context) {
+        TvInputManagerHelper tvInputManagerHelper = new TvInputManagerHelper(context);
+        tvInputManagerHelper.start();
+        return tvInputManagerHelper;
+    }
+}
diff --git a/src/com/android/tv/modules/TvSingletonsModule.java b/src/com/android/tv/modules/TvSingletonsModule.java
new file mode 100644
index 0000000..f998c08
--- /dev/null
+++ b/src/com/android/tv/modules/TvSingletonsModule.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.modules;
+
+import com.android.tv.TvSingletons;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.ProgramDataManager;
+import dagger.Module;
+import dagger.Provides;
+
+/**
+ * Provides bindings for items provided by {@link TvSingletons}.
+ *
+ * <p>Use this module to inject items directly instead of using {@code TvSingletons}.
+ */
+@Module
+@SuppressWarnings("deprecation")
+public class TvSingletonsModule {
+    private final TvSingletons mTvSingletons;
+
+    public TvSingletonsModule(TvSingletons mTvSingletons) {
+        this.mTvSingletons = mTvSingletons;
+    }
+
+    @Provides
+    ChannelDataManager providesChannelDataManager() {
+        return mTvSingletons.getChannelDataManager();
+    }
+
+    @Provides
+    ProgramDataManager providesProgramDataManager() {
+        return mTvSingletons.getProgramDataManager();
+    }
+}
diff --git a/src/com/android/tv/onboarding/OnboardingActivity.java b/src/com/android/tv/onboarding/OnboardingActivity.java
index a1cf9de..776ae66 100644
--- a/src/com/android/tv/onboarding/OnboardingActivity.java
+++ b/src/com/android/tv/onboarding/OnboardingActivity.java
@@ -37,6 +37,9 @@
 import com.android.tv.util.OnboardingUtils;
 import com.android.tv.util.SetupUtils;
 import com.android.tv.util.TvInputManagerHelper;
+import dagger.android.AndroidInjection;
+import dagger.android.ContributesAndroidInjector;
+import javax.inject.Inject;
 
 public class OnboardingActivity extends SetupActivity {
     private static final String KEY_INTENT_AFTER_COMPLETION = "key_intent_after_completion";
@@ -47,9 +50,9 @@
 
     private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1;
 
-    private ChannelDataManager mChannelDataManager;
+    @Inject ChannelDataManager mChannelDataManager;
     private TvInputManagerHelper mInputManager;
-    private SetupUtils mSetupUtils;
+    @Inject SetupUtils mSetupUtils;
     private final ChannelDataManager.Listener mChannelListener =
             new ChannelDataManager.Listener() {
                 @Override
@@ -80,12 +83,11 @@
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
+        AndroidInjection.inject(this);
         super.onCreate(savedInstanceState);
         TvSingletons singletons = TvSingletons.getSingletons(this);
         mInputManager = singletons.getTvInputManagerHelper();
-        mSetupUtils = singletons.getSetupUtils();
         if (PermissionUtils.hasAccessAllEpg(this) || PermissionUtils.hasReadTvListings(this)) {
-            mChannelDataManager = singletons.getChannelDataManager();
             // Make the channels of the new inputs which have been setup outside Live TV
             // browsable.
             if (mChannelDataManager.isDbLoadFinished()) {
@@ -148,13 +150,7 @@
 
     private void showMerchantCollection() {
         executeActionWithDelay(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        startActivity(OnboardingUtils.ONLINE_STORE_INTENT);
-                    }
-                },
-                SHOW_RIPPLE_DURATION_MS);
+                () -> startActivity(OnboardingUtils.ONLINE_STORE_INTENT), SHOW_RIPPLE_DURATION_MS);
     }
 
     @Override
@@ -228,4 +224,11 @@
         }
         return false;
     }
+
+    /** Exports {@link OnboardingActivity} for Dagger codegen to create the appropriate injector. */
+    @dagger.Module
+    public abstract static class Module {
+        @ContributesAndroidInjector
+        abstract OnboardingActivity contributeOnboardingActivityInjector();
+    }
 }
diff --git a/src/com/android/tv/onboarding/SetupSourcesFragment.java b/src/com/android/tv/onboarding/SetupSourcesFragment.java
index f032f62..3566c9c 100644
--- a/src/com/android/tv/onboarding/SetupSourcesFragment.java
+++ b/src/com/android/tv/onboarding/SetupSourcesFragment.java
@@ -197,9 +197,13 @@
             mChannelDataManager.addListener(mChannelDataManagerListener);
             super.onCreate(savedInstanceState);
             mParentFragment = (SetupSourcesFragment) getParentFragment();
-            singletons
-                    .getTunerInputController()
-                    .executeNetworkTunerDiscoveryAsyncTask(getContext());
+            if (singletons.getBuiltInTunerManager().isPresent()) {
+                singletons
+                        .getBuiltInTunerManager()
+                        .get()
+                        .getTunerInputController()
+                        .executeNetworkTunerDiscoveryAsyncTask(getContext());
+            }
         }
 
         @Override
@@ -332,7 +336,7 @@
                             .id(ACTION_ONLINE_STORE)
                             .title(getString(R.string.setup_store_action_title))
                             .description(getString(R.string.setup_store_action_description))
-                            .icon(R.drawable.ic_store)
+                            .icon(R.drawable.ic_app_store)
                             .build());
 
             if (newPosition != -1) {
diff --git a/src/com/android/tv/parental/ContentRatingSystem.java b/src/com/android/tv/parental/ContentRatingSystem.java
index 600aaca..d85dd50 100644
--- a/src/com/android/tv/parental/ContentRatingSystem.java
+++ b/src/com/android/tv/parental/ContentRatingSystem.java
@@ -31,13 +31,10 @@
      * A comparator that implements the display order of a group of content rating systems.
      */
     public static final Comparator<ContentRatingSystem> DISPLAY_NAME_COMPARATOR =
-            new Comparator<ContentRatingSystem>() {
-                @Override
-                public int compare(ContentRatingSystem s1, ContentRatingSystem s2) {
-                    String name1 = s1.getDisplayName();
-                    String name2 = s2.getDisplayName();
-                    return name1.compareTo(name2);
-                }
+            (ContentRatingSystem s1, ContentRatingSystem s2) -> {
+                String name1 = s1.getDisplayName();
+                String name2 = s2.getDisplayName();
+                return name1.compareTo(name2);
             };
 
     private static final String DELIMITER = "/";
diff --git a/src/com/android/tv/parental/ParentalControlSettings.java b/src/com/android/tv/parental/ParentalControlSettings.java
index db1f0a4..b41b160 100644
--- a/src/com/android/tv/parental/ParentalControlSettings.java
+++ b/src/com/android/tv/parental/ParentalControlSettings.java
@@ -24,6 +24,7 @@
 import com.android.tv.parental.ContentRatingSystem.SubRating;
 import com.android.tv.util.TvSettings;
 import com.android.tv.util.TvSettings.ContentRatingLevel;
+import com.google.common.collect.ImmutableList;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -160,6 +161,26 @@
     }
 
     /**
+     * Checks whether any of given ratings is blocked and returns the first blocked rating.
+     *
+     * @param ratings The array of ratings to check
+     * @return The {@link TvContentRating} that is blocked.
+     */
+    public TvContentRating getBlockedRating(ImmutableList<TvContentRating> ratings) {
+        if (ratings == null || ratings.isEmpty()) {
+            return mTvInputManager.isRatingBlocked(TvContentRating.UNRATED)
+                    ? TvContentRating.UNRATED
+                    : null;
+        }
+        for (TvContentRating rating : ratings) {
+            if (mTvInputManager.isRatingBlocked(rating)) {
+                return rating;
+            }
+        }
+        return null;
+    }
+
+    /**
      * Sets the blocked status of a given content rating.
      *
      * <p>Note that a call to this method automatically changes the current rating level to {@code
@@ -178,34 +199,14 @@
     /**
      * Checks whether any of given ratings is blocked.
      *
-     * @param ratings The array of ratings to check
+     * @param ratings The list of ratings to check
      * @return {@code true} if a rating is blocked, {@code false} otherwise.
      */
-    public boolean isRatingBlocked(TvContentRating[] ratings) {
+    public boolean isRatingBlocked(ImmutableList<TvContentRating> ratings) {
         return getBlockedRating(ratings) != null;
     }
 
     /**
-     * Checks whether any of given ratings is blocked and returns the first blocked rating.
-     *
-     * @param ratings The array of ratings to check
-     * @return The {@link TvContentRating} that is blocked.
-     */
-    public TvContentRating getBlockedRating(TvContentRating[] ratings) {
-        if (ratings == null || ratings.length <= 0) {
-            return mTvInputManager.isRatingBlocked(TvContentRating.UNRATED)
-                    ? TvContentRating.UNRATED
-                    : null;
-        }
-        for (TvContentRating rating : ratings) {
-            if (mTvInputManager.isRatingBlocked(rating)) {
-                return rating;
-            }
-        }
-        return null;
-    }
-
-    /**
      * Checks whether a given rating is blocked by the user or not.
      *
      * @param contentRatingSystem The content rating system where the given rating belongs.
diff --git a/src/com/android/tv/perf/EventNames.java b/src/com/android/tv/perf/EventNames.java
index 54745f3..4d21d6d 100644
--- a/src/com/android/tv/perf/EventNames.java
+++ b/src/com/android/tv/perf/EventNames.java
@@ -25,31 +25,39 @@
  * Constants for performance event names.
  *
  * <p>Only constants are used to insure no PII is sent.
- *
+
  */
 public final class EventNames {
 
     @Retention(SOURCE)
     @StringDef({
-        APPLICATION_ONCREATE,
         FETCH_EPG_TASK,
-        MAIN_ACTIVITY_ONCREATE,
-        MAIN_ACTIVITY_ONSTART,
-        MAIN_ACTIVITY_ONRESUME,
-        ON_DEVICE_SEARCH
+        ON_DEVICE_SEARCH,
+        PROGRAM_GUIDE_SHOW,
+        PROGRAM_DATA_MANAGER_PROGRAMS_PREFETCH_TASK_DO_IN_BACKGROUND,
+        PROGRAM_GUIDE_SHOW_FROM_EMPTY_CACHE,
+        PROGRAM_GUIDE_SCROLL_HORIZONTALLY,
+        PROGRAM_GUIDE_SCROLL_VERTICALLY,
+        MEMORY_ON_PROGRAM_GUIDE_CLOSE
     })
     public @interface EventName {}
 
-    public static final String APPLICATION_ONCREATE = "Application.onCreate";
     public static final String FETCH_EPG_TASK = "FetchEpgTask";
-    public static final String MAIN_ACTIVITY_ONCREATE = "MainActivity.onCreate";
-    public static final String MAIN_ACTIVITY_ONSTART = "MainActivity.onStart";
-    public static final String MAIN_ACTIVITY_ONRESUME = "MainActivity.onResume";
     /**
      * Event name for query running time of on-device search in {@link
      * com.android.tv.search.LocalSearchProvider}.
      */
     public static final String ON_DEVICE_SEARCH = "OnDeviceSearch";
 
+    public static final String PROGRAM_GUIDE_SHOW = "ProgramGuide.show";
+    public static final String PROGRAM_DATA_MANAGER_PROGRAMS_PREFETCH_TASK_DO_IN_BACKGROUND =
+            "ProgramDataManager.ProgramsPrefetchTask.doInBackground";
+    public static final String PROGRAM_GUIDE_SHOW_FROM_EMPTY_CACHE =
+            "ProgramGuide.show.fromEmptyCache";
+    public static final String PROGRAM_GUIDE_SCROLL_HORIZONTALLY =
+            "ProgramGuide.scroll.horizontally";
+    public static final String PROGRAM_GUIDE_SCROLL_VERTICALLY = "ProgramGuide.scroll.vertically";
+    public static final String MEMORY_ON_PROGRAM_GUIDE_CLOSE = "ProgramGuide.memory.close";
+
     private EventNames() {}
 }
diff --git a/src/com/android/tv/perf/PerformanceMonitor.java b/src/com/android/tv/perf/PerformanceMonitor.java
index 111aa85..b1ae759 100644
--- a/src/com/android/tv/perf/PerformanceMonitor.java
+++ b/src/com/android/tv/perf/PerformanceMonitor.java
@@ -19,6 +19,7 @@
 import static com.android.tv.perf.EventNames.EventName;
 
 import android.content.Context;
+import com.google.errorprone.annotations.CompileTimeConstant;
 
 /** Measures Performance. */
 public interface PerformanceMonitor {
@@ -34,7 +35,7 @@
      *
      * @param eventName to record
      */
-    void recordMemory(@EventName String eventName);
+    void recordMemory(@EventName @CompileTimeConstant String eventName);
 
     /**
      * Starts a timer for a global event to allow measuring the event's latency across activities If
@@ -42,7 +43,7 @@
      *
      * @param eventName for which the timer starts
      */
-    void startGlobalTimer(@EventName String eventName);
+    void startGlobalTimer(@EventName @CompileTimeConstant String eventName);
 
     /**
      * Stops a cross activities timer for a specific eventName and records the timer duration. If no
@@ -50,7 +51,7 @@
      *
      * @param eventName for which the timer stops
      */
-    void stopGlobalTimer(@EventName String eventName);
+    void stopGlobalTimer(@EventName @CompileTimeConstant String eventName);
 
     /**
      * Starts a timer to record latency of a specific scenario or event. Use this method to track
@@ -69,7 +70,7 @@
      * @param event that needs to be stopped
      * @param eventName for which the timer stops. This must be constant with no PII.
      */
-    void stopTimer(TimerEvent event, @EventName String eventName);
+    void stopTimer(TimerEvent event, @EventName @CompileTimeConstant String eventName);
 
     /**
      * Starts recording jank for a specific scenario or event.
@@ -79,14 +80,14 @@
      *
      * @param eventName of the event for which tracking is started
      */
-    void startJankRecorder(@EventName String eventName);
+    void startJankRecorder(@EventName @CompileTimeConstant String eventName);
 
     /**
      * Stops recording jank for a specific event and records the jank event.
      *
      * @param eventName of the event that needs to be stopped
      */
-    void stopJankRecorder(@EventName String eventName);
+    void stopJankRecorder(@EventName @CompileTimeConstant String eventName);
 
     /**
      * Starts activity to display PerformanceMonitor events recorded in local database for debug
diff --git a/src/com/android/tv/perf/PerformanceMonitorManager.java b/src/com/android/tv/perf/PerformanceMonitorManager.java
new file mode 100644
index 0000000..db6667d
--- /dev/null
+++ b/src/com/android/tv/perf/PerformanceMonitorManager.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.perf;
+
+import android.app.Application;
+
+/** Manages the initialization of Performance Monitoring. */
+public interface PerformanceMonitorManager {
+
+    /**
+     * Initializes the {@link com.android.tv.perf.PerformanceMonitor}.
+     *
+     * <p>This should only be called once.
+     */
+    PerformanceMonitor initialize(Application app);
+
+    /**
+     * Returns a lightweight object to help measure both cold and warm startup latency.
+     *
+     * <p>This method is idempotent and lightweight. It can be called multiple times and does not
+     * need to be cached.
+     */
+    StartupMeasure getStartupMeasure();
+}
diff --git a/src/com/android/tv/perf/PerformanceMonitorManagerFactory.java b/src/com/android/tv/perf/PerformanceMonitorManagerFactory.java
new file mode 100644
index 0000000..fe3ea14
--- /dev/null
+++ b/src/com/android/tv/perf/PerformanceMonitorManagerFactory.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.perf;
+
+import com.android.tv.perf.stub.StubPerformanceMonitorManager;
+import javax.inject.Inject;
+
+public final class PerformanceMonitorManagerFactory {
+    private static final PerformanceMonitorManagerFactory INSTANCE =
+            new PerformanceMonitorManagerFactory();
+
+    @Inject
+    public PerformanceMonitorManagerFactory() {}
+
+    public static PerformanceMonitorManager create() {
+        return INSTANCE.get();
+    }
+
+    public PerformanceMonitorManager get() {
+        return new StubPerformanceMonitorManager();
+    }
+}
diff --git a/src/com/android/tv/perf/StartupMeasure.java b/src/com/android/tv/perf/StartupMeasure.java
new file mode 100644
index 0000000..5cf183c
--- /dev/null
+++ b/src/com/android/tv/perf/StartupMeasure.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.perf;
+
+import android.app.Activity;
+import android.app.Application;
+
+/**
+ * Measures App startup. This interface is lightweight to help measure both cold and warm startup
+ * latency. Implementations must not throw any Exception.
+ */
+public interface StartupMeasure {
+
+    /** To be be placed as the first static block in the app's Application class. */
+    void onAppClassLoaded();
+
+    /**
+     * To be placed in your {@link Application#onCreate} to let Performance Monitoring know when
+     * this happen.
+     */
+    void onAppCreate(Application application);
+
+    /**
+     * To be placed in an initialization block of your {@link Activity} to let Performance
+     * Monitoring know when this activity is instantiated. Please note that this initialization
+     * block should be before other initialization blocks (if any) in your activity class.
+     */
+    void onActivityInit();
+}
diff --git a/src/com/android/tv/perf/StubPerformanceMonitor.java b/src/com/android/tv/perf/stub/StubPerformanceMonitor.java
similarity index 90%
rename from src/com/android/tv/perf/StubPerformanceMonitor.java
rename to src/com/android/tv/perf/stub/StubPerformanceMonitor.java
index 3742a2a..80c2f6c 100644
--- a/src/com/android/tv/perf/StubPerformanceMonitor.java
+++ b/src/com/android/tv/perf/stub/StubPerformanceMonitor.java
@@ -14,20 +14,17 @@
  * limitations under the License.
  */
 
-package com.android.tv.perf;
+package com.android.tv.perf.stub;
 
-import android.app.Application;
 import android.content.Context;
+import com.android.tv.perf.PerformanceMonitor;
+import com.android.tv.perf.TimerEvent;
 
 /** Do nothing implementation of {@link PerformanceMonitor}. */
 public final class StubPerformanceMonitor implements PerformanceMonitor {
 
     private static final TimerEvent TIMER_EVENT = new TimerEvent() {};
 
-    public static PerformanceMonitor initialize(Application app) {
-        return new StubPerformanceMonitor();
-    }
-
     @Override
     public void startMemoryMonitor() {}
 
diff --git a/src/com/android/tv/perf/stub/StubPerformanceMonitorManager.java b/src/com/android/tv/perf/stub/StubPerformanceMonitorManager.java
new file mode 100644
index 0000000..0c26815
--- /dev/null
+++ b/src/com/android/tv/perf/stub/StubPerformanceMonitorManager.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.perf.stub;
+
+import android.app.Application;
+import com.android.tv.perf.PerformanceMonitor;
+import com.android.tv.perf.PerformanceMonitorManager;
+import com.android.tv.perf.StartupMeasure;
+
+/** Manages a stub implementation of Performance Monitoring. */
+public class StubPerformanceMonitorManager implements PerformanceMonitorManager {
+
+    @Override
+    public PerformanceMonitor initialize(Application app) {
+        return new StubPerformanceMonitor();
+    }
+
+    @Override
+    public StartupMeasure getStartupMeasure() {
+        return new StubStartupMeasure();
+    }
+}
diff --git a/src/com/android/tv/perf/stub/StubStartupMeasure.java b/src/com/android/tv/perf/stub/StubStartupMeasure.java
new file mode 100644
index 0000000..d441226
--- /dev/null
+++ b/src/com/android/tv/perf/stub/StubStartupMeasure.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.perf.stub;
+
+import android.app.Application;
+import com.android.tv.perf.StartupMeasure;
+
+/** Stub implementation of {@link StartupMeasure} */
+public class StubStartupMeasure implements StartupMeasure {
+
+    @Override
+    public void onAppClassLoaded() {}
+
+    @Override
+    public void onAppCreate(Application application) {}
+
+    @Override
+    public void onActivityInit() {}
+}
diff --git a/src/com/android/tv/receiver/BootCompletedReceiver.java b/src/com/android/tv/receiver/BootCompletedReceiver.java
index d8528bb..0eb03be 100644
--- a/src/com/android/tv/receiver/BootCompletedReceiver.java
+++ b/src/com/android/tv/receiver/BootCompletedReceiver.java
@@ -25,10 +25,10 @@
 import android.util.Log;
 import com.android.tv.Starter;
 import com.android.tv.TvActivity;
-import com.android.tv.TvFeatures;
 import com.android.tv.TvSingletons;
 import com.android.tv.dvr.recorder.DvrRecordingService;
 import com.android.tv.dvr.recorder.RecordingScheduler;
+import com.android.tv.features.TvFeatures;
 import com.android.tv.recommendation.ChannelPreviewUpdater;
 import com.android.tv.recommendation.NotificationService;
 import com.android.tv.util.OnboardingUtils;
@@ -70,7 +70,7 @@
         // Grant permission to already set up packages after the system has finished booting.
         SetupUtils.grantEpgPermissionToSetUpPackages(context);
 
-        if (TvFeatures.UNHIDE.isEnabled(context)) {
+        if (TvFeatures.UNHIDE.isEnabled(context.getApplicationContext())) {
             if (OnboardingUtils.isFirstBoot(context)) {
                 // Enable the application if this is the first "unhide" feature is enabled just in
                 // case when the app has been disabled before.
diff --git a/src/com/android/tv/receiver/PackageIntentsReceiver.java b/src/com/android/tv/receiver/PackageIntentsReceiver.java
index 07f5d6b..5bc6d72 100644
--- a/src/com/android/tv/receiver/PackageIntentsReceiver.java
+++ b/src/com/android/tv/receiver/PackageIntentsReceiver.java
@@ -22,8 +22,8 @@
 import android.net.Uri;
 import android.util.Log;
 import com.android.tv.Starter;
-import com.android.tv.TvFeatures;
 import com.android.tv.TvSingletons;
+import com.android.tv.features.TvFeatures;
 import com.android.tv.util.Partner;
 import com.google.android.tv.partner.support.EpgContract;
 
diff --git a/src/com/android/tv/recommendation/ChannelPreviewUpdater.java b/src/com/android/tv/recommendation/ChannelPreviewUpdater.java
index 410b825..2590a33 100644
--- a/src/com/android/tv/recommendation/ChannelPreviewUpdater.java
+++ b/src/com/android/tv/recommendation/ChannelPreviewUpdater.java
@@ -25,9 +25,9 @@
 import android.os.AsyncTask;
 import android.os.Build;
 import android.support.annotation.RequiresApi;
-import android.support.media.tv.TvContractCompat;
 import android.text.TextUtils;
 import android.util.Log;
+import androidx.tvprovider.media.tv.TvContractCompat;
 import com.android.tv.Starter;
 import com.android.tv.TvSingletons;
 import com.android.tv.data.PreviewDataManager;
@@ -169,18 +169,23 @@
             @Override
             protected Set<Program> doInBackground(Void... params) {
                 Set<Program> programs = new HashSet<>();
-                List<Channel> channels = new ArrayList<>(mRecommender.recommendChannels());
-                for (Channel channel : channels) {
-                    if (channel.isPhysicalTunerChannel()) {
-                        final Program program = Utils.getCurrentProgram(mContext, channel.getId());
-                        if (program != null
-                                && isChannelRecommendationApplicable(channel, program)) {
-                            programs.add(program);
-                            if (programs.size() >= RECOMMENDATION_COUNT) {
-                                break;
+                try {
+                    List<Channel> channels = new ArrayList<>(mRecommender.recommendChannels());
+                    for (Channel channel : channels) {
+                        if (channel.isPhysicalTunerChannel()) {
+                            final Program program =
+                                    Utils.getCurrentProgram(mContext, channel.getId());
+                            if (program != null
+                                    && isChannelRecommendationApplicable(channel, program)) {
+                                programs.add(program);
+                                if (programs.size() >= RECOMMENDATION_COUNT) {
+                                    break;
+                                }
                             }
                         }
                     }
+                } catch (Exception e) {
+                    Log.w(TAG, "Can't update preview data", e);
                 }
                 return programs;
             }
@@ -241,6 +246,17 @@
                                 }
                             }
                         });
+            } else if (mJobService != null && mJobParams != null) {
+                if (DEBUG) {
+                    Log.d(
+                            TAG,
+                            "Preview channel not created because there is only "
+                                    + programs.size()
+                                    + " programs");
+                }
+                mJobService.jobFinished(mJobParams, false);
+                mJobService = null;
+                mJobParams = null;
             }
         } else {
             updatePreviewProgramsForPreviewChannel(
diff --git a/src/com/android/tv/recommendation/RecommendationDataManager.java b/src/com/android/tv/recommendation/RecommendationDataManager.java
index 649920f..fc20031 100644
--- a/src/com/android/tv/recommendation/RecommendationDataManager.java
+++ b/src/com/android/tv/recommendation/RecommendationDataManager.java
@@ -33,6 +33,7 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.WorkerThread;
+import android.util.Log;
 import com.android.tv.TvSingletons;
 import com.android.tv.common.WeakHandler;
 import com.android.tv.common.util.PermissionUtils;
@@ -52,6 +53,7 @@
 
 /** Manages teh data need to make recommendations. */
 public class RecommendationDataManager implements WatchedHistoryManager.Listener {
+    private static final String TAG = "RecommendationDataManag";
     private static final int MSG_START = 1000;
     private static final int MSG_STOP = 1001;
     private static final int MSG_UPDATE_CHANNELS = 1002;
@@ -187,13 +189,7 @@
         mMainHandler = new RecommendationMainHandler(Looper.getMainLooper(), this);
         mContentObserver = new RecommendationContentObserver(mHandler);
         mChannelDataManager = TvSingletons.getSingletons(mContext).getChannelDataManager();
-        runOnMainThread(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        start();
-                    }
-                });
+        runOnMainThread(this::start);
     }
 
     /**
@@ -202,13 +198,10 @@
      */
     public void release(@NonNull final Listener listener) {
         runOnMainThread(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        removeListener(listener);
-                        if (mListeners.size() == 0) {
-                            stop();
-                        }
+                () -> {
+                    removeListener(listener);
+                    if (mListeners.size() == 0) {
+                        stop();
                     }
                 });
     }
@@ -257,13 +250,7 @@
     }
 
     private void addListener(Listener listener) {
-        runOnMainThread(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        mListeners.add(listener);
-                    }
-                });
+        runOnMainThread(() -> mListeners.add(listener));
     }
 
     @MainThread
@@ -347,18 +334,18 @@
                     history.add(createWatchedProgramFromWatchedProgramCursor(cursor));
                 } while (cursor.moveToPrevious());
             }
+        } catch (Exception e) {
+            Log.e(TAG, "Error trying to load watch history from " + uri, e);
+            return;
         }
         for (WatchedProgram watchedProgram : history) {
             final ChannelRecord channelRecord =
                     updateChannelRecordFromWatchedProgram(watchedProgram);
             if (mChannelRecordMapLoaded && channelRecord != null) {
                 runOnMainThread(
-                        new Runnable() {
-                            @Override
-                            public void run() {
-                                for (Listener l : mListeners) {
-                                    l.onNewWatchLog(channelRecord);
-                                }
+                        () -> {
+                            for (Listener l : mListeners) {
+                                l.onNewWatchLog(channelRecord);
                             }
                         });
             }
@@ -397,12 +384,9 @@
                         convertFromWatchedHistoryManagerRecords(watchedRecord));
         if (mChannelRecordMapLoaded && channelRecord != null) {
             runOnMainThread(
-                    new Runnable() {
-                        @Override
-                        public void run() {
-                            for (Listener l : mListeners) {
-                                l.onNewWatchLog(channelRecord);
-                            }
+                    () -> {
+                        for (Listener l : mListeners) {
+                            l.onNewWatchLog(channelRecord);
                         }
                     });
         }
@@ -441,24 +425,18 @@
     private void onNotifyChannelRecordMapLoaded() {
         mChannelRecordMapLoaded = true;
         runOnMainThread(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        for (Listener l : mListeners) {
-                            l.onChannelRecordLoaded();
-                        }
+                () -> {
+                    for (Listener l : mListeners) {
+                        l.onChannelRecordLoaded();
                     }
                 });
     }
 
     private void onNotifyChannelRecordMapChanged() {
         runOnMainThread(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        for (Listener l : mListeners) {
-                            l.onChannelRecordChanged();
-                        }
+                () -> {
+                    for (Listener l : mListeners) {
+                        l.onChannelRecordChanged();
                     }
                 });
     }
diff --git a/src/com/android/tv/search/AutoValue_LocalSearchProvider_SearchResult.java b/src/com/android/tv/search/AutoValue_LocalSearchProvider_SearchResult.java
deleted file mode 100644
index 528096d..0000000
--- a/src/com/android/tv/search/AutoValue_LocalSearchProvider_SearchResult.java
+++ /dev/null
@@ -1,363 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-
-
-package com.android.tv.search;
-
-import android.support.annotation.Nullable;
-
-/**
- * Hand copy of generated Autovalue class.
- *
- * TODO get autovalue working
- */
-
-final class AutoValue_LocalSearchProvider_SearchResult extends LocalSearchProvider.SearchResult {
-
-    private final long channelId;
-    private final String channelNumber;
-    private final String title;
-    private final String description;
-    private final String imageUri;
-    private final String intentAction;
-    private final String intentData;
-    private final String intentExtraData;
-    private final String contentType;
-    private final boolean isLive;
-    private final int videoWidth;
-    private final int videoHeight;
-    private final long duration;
-    private final int progressPercentage;
-
-    private AutoValue_LocalSearchProvider_SearchResult(
-            long channelId,
-            @Nullable String channelNumber,
-            @Nullable String title,
-            @Nullable String description,
-            @Nullable String imageUri,
-            @Nullable String intentAction,
-            @Nullable String intentData,
-            @Nullable String intentExtraData,
-            @Nullable String contentType,
-            boolean isLive,
-            int videoWidth,
-            int videoHeight,
-            long duration,
-            int progressPercentage) {
-        this.channelId = channelId;
-        this.channelNumber = channelNumber;
-        this.title = title;
-        this.description = description;
-        this.imageUri = imageUri;
-        this.intentAction = intentAction;
-        this.intentData = intentData;
-        this.intentExtraData = intentExtraData;
-        this.contentType = contentType;
-        this.isLive = isLive;
-        this.videoWidth = videoWidth;
-        this.videoHeight = videoHeight;
-        this.duration = duration;
-        this.progressPercentage = progressPercentage;
-    }
-
-    @Override
-    long getChannelId() {
-        return channelId;
-    }
-
-    @Nullable
-    @Override
-    String getChannelNumber() {
-        return channelNumber;
-    }
-
-    @Nullable
-    @Override
-    String getTitle() {
-        return title;
-    }
-
-    @Nullable
-    @Override
-    String getDescription() {
-        return description;
-    }
-
-    @Nullable
-    @Override
-    String getImageUri() {
-        return imageUri;
-    }
-
-    @Nullable
-    @Override
-    String getIntentAction() {
-        return intentAction;
-    }
-
-    @Nullable
-    @Override
-    String getIntentData() {
-        return intentData;
-    }
-
-    @Nullable
-    @Override
-    String getIntentExtraData() {
-        return intentExtraData;
-    }
-
-    @Nullable
-    @Override
-    String getContentType() {
-        return contentType;
-    }
-
-    @Override
-    boolean getIsLive() {
-        return isLive;
-    }
-
-    @Override
-    int getVideoWidth() {
-        return videoWidth;
-    }
-
-    @Override
-    int getVideoHeight() {
-        return videoHeight;
-    }
-
-    @Override
-    long getDuration() {
-        return duration;
-    }
-
-    @Override
-    int getProgressPercentage() {
-        return progressPercentage;
-    }
-
-    @Override
-    public String toString() {
-        return "SearchResult{"
-                + "channelId=" + channelId + ", "
-                + "channelNumber=" + channelNumber + ", "
-                + "title=" + title + ", "
-                + "description=" + description + ", "
-                + "imageUri=" + imageUri + ", "
-                + "intentAction=" + intentAction + ", "
-                + "intentData=" + intentData + ", "
-                + "intentExtraData=" + intentExtraData + ", "
-                + "contentType=" + contentType + ", "
-                + "isLive=" + isLive + ", "
-                + "videoWidth=" + videoWidth + ", "
-                + "videoHeight=" + videoHeight + ", "
-                + "duration=" + duration + ", "
-                + "progressPercentage=" + progressPercentage
-                + "}";
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (o == this) {
-            return true;
-        }
-        if (o instanceof LocalSearchProvider.SearchResult) {
-            LocalSearchProvider.SearchResult that = (LocalSearchProvider.SearchResult) o;
-            return (this.channelId == that.getChannelId())
-                    && ((this.channelNumber == null) ? (that.getChannelNumber() == null) : this.channelNumber.equals(that.getChannelNumber()))
-                    && ((this.title == null) ? (that.getTitle() == null) : this.title.equals(that.getTitle()))
-                    && ((this.description == null) ? (that.getDescription() == null) : this.description.equals(that.getDescription()))
-                    && ((this.imageUri == null) ? (that.getImageUri() == null) : this.imageUri.equals(that.getImageUri()))
-                    && ((this.intentAction == null) ? (that.getIntentAction() == null) : this.intentAction.equals(that.getIntentAction()))
-                    && ((this.intentData == null) ? (that.getIntentData() == null) : this.intentData.equals(that.getIntentData()))
-                    && ((this.intentExtraData == null) ? (that.getIntentExtraData() == null) : this.intentExtraData.equals(that.getIntentExtraData()))
-                    && ((this.contentType == null) ? (that.getContentType() == null) : this.contentType.equals(that.getContentType()))
-                    && (this.isLive == that.getIsLive())
-                    && (this.videoWidth == that.getVideoWidth())
-                    && (this.videoHeight == that.getVideoHeight())
-                    && (this.duration == that.getDuration())
-                    && (this.progressPercentage == that.getProgressPercentage());
-        }
-        return false;
-    }
-
-    @Override
-    public int hashCode() {
-        int h$ = 1;
-        h$ *= 1000003;
-        h$ ^= (int) ((channelId >>> 32) ^ channelId);
-        h$ *= 1000003;
-        h$ ^= (channelNumber == null) ? 0 : channelNumber.hashCode();
-        h$ *= 1000003;
-        h$ ^= (title == null) ? 0 : title.hashCode();
-        h$ *= 1000003;
-        h$ ^= (description == null) ? 0 : description.hashCode();
-        h$ *= 1000003;
-        h$ ^= (imageUri == null) ? 0 : imageUri.hashCode();
-        h$ *= 1000003;
-        h$ ^= (intentAction == null) ? 0 : intentAction.hashCode();
-        h$ *= 1000003;
-        h$ ^= (intentData == null) ? 0 : intentData.hashCode();
-        h$ *= 1000003;
-        h$ ^= (intentExtraData == null) ? 0 : intentExtraData.hashCode();
-        h$ *= 1000003;
-        h$ ^= (contentType == null) ? 0 : contentType.hashCode();
-        h$ *= 1000003;
-        h$ ^= isLive ? 1231 : 1237;
-        h$ *= 1000003;
-        h$ ^= videoWidth;
-        h$ *= 1000003;
-        h$ ^= videoHeight;
-        h$ *= 1000003;
-        h$ ^= (int) ((duration >>> 32) ^ duration);
-        h$ *= 1000003;
-        h$ ^= progressPercentage;
-        return h$;
-    }
-
-    static final class Builder extends LocalSearchProvider.SearchResult.Builder {
-        private Long channelId;
-        private String channelNumber;
-        private String title;
-        private String description;
-        private String imageUri;
-        private String intentAction;
-        private String intentData;
-        private String intentExtraData;
-        private String contentType;
-        private Boolean isLive;
-        private Integer videoWidth;
-        private Integer videoHeight;
-        private Long duration;
-        private Integer progressPercentage;
-        Builder() {
-        }
-        @Override
-        LocalSearchProvider.SearchResult.Builder setChannelId(long channelId) {
-            this.channelId = channelId;
-            return this;
-        }
-        @Override
-        LocalSearchProvider.SearchResult.Builder setChannelNumber(@Nullable String channelNumber) {
-            this.channelNumber = channelNumber;
-            return this;
-        }
-        @Override
-        LocalSearchProvider.SearchResult.Builder setTitle(@Nullable String title) {
-            this.title = title;
-            return this;
-        }
-        @Override
-        LocalSearchProvider.SearchResult.Builder setDescription(@Nullable String description) {
-            this.description = description;
-            return this;
-        }
-        @Override
-        LocalSearchProvider.SearchResult.Builder setImageUri(@Nullable String imageUri) {
-            this.imageUri = imageUri;
-            return this;
-        }
-        @Override
-        LocalSearchProvider.SearchResult.Builder setIntentAction(@Nullable String intentAction) {
-            this.intentAction = intentAction;
-            return this;
-        }
-        @Override
-        LocalSearchProvider.SearchResult.Builder setIntentData(@Nullable String intentData) {
-            this.intentData = intentData;
-            return this;
-        }
-        @Override
-        LocalSearchProvider.SearchResult.Builder setIntentExtraData(@Nullable String intentExtraData) {
-            this.intentExtraData = intentExtraData;
-            return this;
-        }
-        @Override
-        LocalSearchProvider.SearchResult.Builder setContentType(@Nullable String contentType) {
-            this.contentType = contentType;
-            return this;
-        }
-        @Override
-        LocalSearchProvider.SearchResult.Builder setIsLive(boolean isLive) {
-            this.isLive = isLive;
-            return this;
-        }
-        @Override
-        LocalSearchProvider.SearchResult.Builder setVideoWidth(int videoWidth) {
-            this.videoWidth = videoWidth;
-            return this;
-        }
-        @Override
-        LocalSearchProvider.SearchResult.Builder setVideoHeight(int videoHeight) {
-            this.videoHeight = videoHeight;
-            return this;
-        }
-        @Override
-        LocalSearchProvider.SearchResult.Builder setDuration(long duration) {
-            this.duration = duration;
-            return this;
-        }
-        @Override
-        LocalSearchProvider.SearchResult.Builder setProgressPercentage(int progressPercentage) {
-            this.progressPercentage = progressPercentage;
-            return this;
-        }
-        @Override
-        LocalSearchProvider.SearchResult build() {
-            String missing = "";
-            if (this.channelId == null) {
-                missing += " channelId";
-            }
-            if (this.isLive == null) {
-                missing += " isLive";
-            }
-            if (this.videoWidth == null) {
-                missing += " videoWidth";
-            }
-            if (this.videoHeight == null) {
-                missing += " videoHeight";
-            }
-            if (this.duration == null) {
-                missing += " duration";
-            }
-            if (this.progressPercentage == null) {
-                missing += " progressPercentage";
-            }
-            if (!missing.isEmpty()) {
-                throw new IllegalStateException("Missing required properties:" + missing);
-            }
-            return new AutoValue_LocalSearchProvider_SearchResult(
-                    this.channelId,
-                    this.channelNumber,
-                    this.title,
-                    this.description,
-                    this.imageUri,
-                    this.intentAction,
-                    this.intentData,
-                    this.intentExtraData,
-                    this.contentType,
-                    this.isLive,
-                    this.videoWidth,
-                    this.videoHeight,
-                    this.duration,
-                    this.progressPercentage);
-        }
-    }
-
-}
diff --git a/src/com/android/tv/search/DataManagerSearch.java b/src/com/android/tv/search/DataManagerSearch.java
index 82fb501..a649c0a 100644
--- a/src/com/android/tv/search/DataManagerSearch.java
+++ b/src/com/android/tv/search/DataManagerSearch.java
@@ -34,12 +34,12 @@
 import com.android.tv.search.LocalSearchProvider.SearchResult;
 import com.android.tv.util.MainThreadExecutor;
 import com.android.tv.util.Utils;
+import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
-import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
 
@@ -68,13 +68,7 @@
     public List<SearchResult> search(final String query, final int limit, final int action) {
         Future<List<SearchResult>> future =
                 MainThreadExecutor.getInstance()
-                        .submit(
-                                new Callable<List<SearchResult>>() {
-                                    @Override
-                                    public List<SearchResult> call() throws Exception {
-                                        return searchFromDataManagers(query, limit, action);
-                                    }
-                                });
+                        .submit(() -> searchFromDataManagers(query, limit, action));
 
         try {
             return future.get();
@@ -255,7 +249,7 @@
             result.setIntentData(buildIntentData(channelId));
             result.setContentType(Programs.CONTENT_ITEM_TYPE);
             result.setIsLive(true);
-            result.setProgressPercentage(LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE);
+            result.setProgressPercentage(SearchInterface.PROGRESS_PERCENTAGE_HIDE);
         } else {
             result.setTitle(program.getTitle());
             result.setDescription(
@@ -299,7 +293,7 @@
     private int getProgressPercentage(long startUtcMillis, long endUtcMillis) {
         long current = System.currentTimeMillis();
         if (startUtcMillis > current || endUtcMillis <= current) {
-            return LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE;
+            return SearchInterface.PROGRESS_PERCENTAGE_HIDE;
         }
         return (int) (100 * (current - startUtcMillis) / (endUtcMillis - startUtcMillis));
     }
@@ -308,10 +302,8 @@
         return TvContract.buildChannelUri(channelId).toString();
     }
 
-    private boolean isRatingBlocked(TvContentRating[] ratings) {
-        if (ratings == null
-                || ratings.length == 0
-                || !mTvInputManager.isParentalControlsEnabled()) {
+    private boolean isRatingBlocked(ImmutableList<TvContentRating> ratings) {
+        if (ratings == null || ratings.isEmpty() || !mTvInputManager.isParentalControlsEnabled()) {
             return false;
         }
         for (TvContentRating rating : ratings) {
diff --git a/src/com/android/tv/search/LocalSearchProvider.java b/src/com/android/tv/search/LocalSearchProvider.java
index 97e7f22..5652c98 100644
--- a/src/com/android/tv/search/LocalSearchProvider.java
+++ b/src/com/android/tv/search/LocalSearchProvider.java
@@ -37,6 +37,7 @@
 import com.android.tv.perf.PerformanceMonitor;
 import com.android.tv.perf.TimerEvent;
 import com.android.tv.util.TvUriMatcher;
+import com.google.auto.value.AutoValue;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -48,8 +49,6 @@
     /** The authority for LocalSearchProvider. */
     public static final String AUTHORITY = CommonConstants.BASE_PACKAGE + ".search";
 
-    public static final int PROGRESS_PERCENTAGE_HIDE = -1;
-
     // TODO: Remove this once added to the SearchManager.
     private static final String SUGGEST_COLUMN_PROGRESS_BAR_PERCENTAGE = "progress_bar_percentage";
 
@@ -223,7 +222,7 @@
     }
 
     /** A placeholder to a search result. */
-    // TODO(b/72052568): Get autovalue to work in aosp master
+    @AutoValue
     public abstract static class SearchResult {
         public static Builder builder() {
             // primitive fields cannot be nullable. Set to default;
@@ -236,7 +235,7 @@
                     .setProgressPercentage(0);
         }
 
-        // TODO(b/72052568): Get autovalue to work in aosp master
+        @AutoValue.Builder
         abstract static class Builder {
             abstract Builder setChannelId(long value);
 
diff --git a/src/com/android/tv/search/ProgramGuideSearchFragment.java b/src/com/android/tv/search/ProgramGuideSearchFragment.java
index cb26252..6c94bd3 100644
--- a/src/com/android/tv/search/ProgramGuideSearchFragment.java
+++ b/src/com/android/tv/search/ProgramGuideSearchFragment.java
@@ -84,7 +84,7 @@
                                 createImageLoaderCallback(cardView));
                     } else {
                         cardView.setMainImage(
-                                mMainActivity.getDrawable(R.drawable.ic_live_channels_96x96));
+                                mMainActivity.getDrawable(R.drawable.ic_tv_app_96x96));
                     }
                 }
 
@@ -171,7 +171,7 @@
         View v = super.onCreateView(inflater, container, savedInstanceState);
         v.setBackgroundResource(R.color.program_guide_scrim);
 
-        setBadgeDrawable(mMainActivity.getDrawable(R.drawable.ic_live_channels_96x96));
+        setBadgeDrawable(mMainActivity.getDrawable(R.drawable.ic_tv_app_96x96));
         setSearchResultProvider(mSearchResultProvider);
         setOnItemViewClickedListener(mItemClickedListener);
         return v;
diff --git a/src/com/android/tv/search/SearchInterface.java b/src/com/android/tv/search/SearchInterface.java
index 4866ee8..d16270e 100644
--- a/src/com/android/tv/search/SearchInterface.java
+++ b/src/com/android/tv/search/SearchInterface.java
@@ -26,6 +26,7 @@
     int ACTION_TYPE_SWITCH_CHANNEL = 2;
     int ACTION_TYPE_SWITCH_INPUT = 3;
     int ACTION_TYPE_END = 3;
+    int PROGRESS_PERCENTAGE_HIDE = -1;
 
     /**
      * Search channels, inputs, or programs. This assumes that parental control settings will not be
diff --git a/src/com/android/tv/search/TvProviderSearch.java b/src/com/android/tv/search/TvProviderSearch.java
index 92197f2..8a1f51f 100644
--- a/src/com/android/tv/search/TvProviderSearch.java
+++ b/src/com/android/tv/search/TvProviderSearch.java
@@ -36,6 +36,7 @@
 import com.android.tv.common.util.PermissionUtils;
 import com.android.tv.search.LocalSearchProvider.SearchResult;
 import com.android.tv.util.Utils;
+import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -270,7 +271,7 @@
                     result.setIntentData(buildIntentData(id));
                     result.setContentType(Programs.CONTENT_ITEM_TYPE);
                     result.setIsLive(true);
-                    result.setProgressPercentage(LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE);
+                    result.setProgressPercentage(SearchInterface.PROGRESS_PERCENTAGE_HIDE);
 
                     searchResults.add(result.build());
 
@@ -343,7 +344,7 @@
     private int getProgressPercentage(long startUtcMillis, long endUtcMillis) {
         long current = System.currentTimeMillis();
         if (startUtcMillis > current || endUtcMillis <= current) {
-            return LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE;
+            return SearchInterface.PROGRESS_PERCENTAGE_HIDE;
         }
         return (int) (100 * (current - startUtcMillis) / (endUtcMillis - startUtcMillis));
     }
@@ -481,7 +482,7 @@
         if (TextUtils.isEmpty(ratings) || !mTvInputManager.isParentalControlsEnabled()) {
             return false;
         }
-        TvContentRating[] ratingArray = mTvContentRatingCache.getRatings(ratings);
+        ImmutableList<TvContentRating> ratingArray = mTvContentRatingCache.getRatings(ratings);
         if (ratingArray != null) {
             for (TvContentRating r : ratingArray) {
                 if (mTvInputManager.isRatingBlocked(r)) {
diff --git a/src/com/android/tv/setup/SystemSetupActivity.java b/src/com/android/tv/setup/SystemSetupActivity.java
index c6b10e5..b2160b3 100644
--- a/src/com/android/tv/setup/SystemSetupActivity.java
+++ b/src/com/android/tv/setup/SystemSetupActivity.java
@@ -64,13 +64,7 @@
 
     private void showMerchantCollection() {
         executeActionWithDelay(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        startActivity(OnboardingUtils.ONLINE_STORE_INTENT);
-                    }
-                },
-                SHOW_RIPPLE_DURATION_MS);
+                () -> startActivity(OnboardingUtils.ONLINE_STORE_INTENT), SHOW_RIPPLE_DURATION_MS);
     }
 
     @Override
diff --git a/src/com/android/tv/tuner/TunerInputController.java b/src/com/android/tv/tuner/TunerInputController.java
deleted file mode 100644
index 02611bb..0000000
--- a/src/com/android/tv/tuner/TunerInputController.java
+++ /dev/null
@@ -1,556 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tv.tuner;
-
-import android.app.AlarmManager;
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.hardware.usb.UsbDevice;
-import android.hardware.usb.UsbManager;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.os.SystemClock;
-import android.preference.PreferenceManager;
-import android.support.annotation.NonNull;
-import android.text.TextUtils;
-import android.util.Log;
-import android.widget.Toast;
-import com.android.tv.R;
-import com.android.tv.Starter;
-import com.android.tv.TvApplication;
-import com.android.tv.TvSingletons;
-import com.android.tv.common.BuildConfig;
-import com.android.tv.common.util.SystemPropertiesProxy;
-
-
-import com.android.tv.tuner.setup.BaseTunerSetupActivity;
-import com.android.tv.tuner.util.TunerInputInfoUtils;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Controls the package visibility of {@link BaseTunerTvInputService}.
- *
- * <p>Listens to broadcast intent for {@link Intent#ACTION_BOOT_COMPLETED}, {@code
- * UsbManager.ACTION_USB_DEVICE_ATTACHED}, and {@code UsbManager.ACTION_USB_DEVICE_ATTACHED} to
- * update the connection status of the supported USB TV tuners.
- */
-public class TunerInputController {
-    private static final boolean DEBUG = false;
-    private static final String TAG = "TunerInputController";
-    private static final String PREFERENCE_IS_NETWORK_TUNER_ATTACHED = "network_tuner";
-    private static final String SECURITY_PATCH_LEVEL_KEY = "ro.build.version.security_patch";
-    private static final String SECURITY_PATCH_LEVEL_FORMAT = "yyyy-MM-dd";
-    private static final String PLAY_STORE_LINK_TEMPLATE = "market://details?id=%s";
-
-    /** Action of {@link Intent} to check network connection repeatedly when it is necessary. */
-    private static final String CHECKING_NETWORK_TUNER_STATUS =
-            "com.android.tv.action.CHECKING_NETWORK_TUNER_STATUS";
-
-    private static final String EXTRA_CHECKING_DURATION =
-            "com.android.tv.action.extra.CHECKING_DURATION";
-    private static final String EXTRA_DEVICE_IP = "com.android.tv.action.extra.DEVICE_IP";
-
-    private static final long INITIAL_CHECKING_DURATION_MS = TimeUnit.SECONDS.toMillis(10);
-    private static final long MAXIMUM_CHECKING_DURATION_MS = TimeUnit.MINUTES.toMillis(10);
-    private static final String NOTIFICATION_CHANNEL_ID = "tuner_discovery_notification";
-
-    // TODO: Load settings from XML file
-    private static final TunerDevice[] TUNER_DEVICES = {
-        new TunerDevice(0x2040, 0xb123, null), // WinTV-HVR-955Q
-        new TunerDevice(0x07ca, 0x0837, null), // AverTV Volar Hybrid Q
-        // WinTV-dualHD (bulk) will be supported after 2017 April security patch.
-        new TunerDevice(0x2040, 0x826d, "2017-04-01"), // WinTV-dualHD (bulk)
-        new TunerDevice(0x2040, 0x0264, null),
-    };
-
-    private static final int MSG_ENABLE_INPUT_SERVICE = 1000;
-    private static final long DVB_DRIVER_CHECK_DELAY_MS = 300;
-
-    private final ComponentName usbTunerComponent;
-    private final ComponentName networkTunerComponent;
-    private final ComponentName builtInTunerComponent;
-    private final Map<TunerDevice, ComponentName> mTunerServiceMapping = new HashMap<>();
-
-    private final Map<ComponentName, String> mTunerApplicationNames = new HashMap<>();
-    private final Map<ComponentName, String> mNotificationMessages = new HashMap<>();
-    private final Map<ComponentName, Bitmap> mNotificationLargeIcons = new HashMap<>();
-
-    private final CheckDvbDeviceHandler mHandler = new CheckDvbDeviceHandler(this);
-
-    public TunerInputController(ComponentName embeddedTuner) {
-        usbTunerComponent = embeddedTuner;
-        networkTunerComponent = usbTunerComponent;
-        builtInTunerComponent = usbTunerComponent;
-        for (TunerDevice device : TUNER_DEVICES) {
-            mTunerServiceMapping.put(device, usbTunerComponent);
-        }
-    }
-
-    /** Checks status of USB devices to see if there are available USB tuners connected. */
-    public void onCheckingUsbTunerStatus(Context context, String action) {
-        onCheckingUsbTunerStatus(context, action, mHandler);
-    }
-
-    private void onCheckingUsbTunerStatus(
-            Context context, String action, @NonNull CheckDvbDeviceHandler handler) {
-        Set<TunerDevice> connectedUsbTuners = getConnectedUsbTuners(context);
-        handler.removeMessages(MSG_ENABLE_INPUT_SERVICE);
-        if (!connectedUsbTuners.isEmpty()) {
-            // Need to check if DVB driver is accessible. Since the driver creation
-            // could be happen after the USB event, delay the checking by
-            // DVB_DRIVER_CHECK_DELAY_MS.
-            handler.sendMessageDelayed(
-                    handler.obtainMessage(MSG_ENABLE_INPUT_SERVICE, context),
-                    DVB_DRIVER_CHECK_DELAY_MS);
-        } else {
-            handleTunerStatusChanged(
-                    context,
-                    false,
-                    connectedUsbTuners,
-                    TextUtils.equals(action, UsbManager.ACTION_USB_DEVICE_DETACHED)
-                            ? TunerHal.TUNER_TYPE_USB
-                            : null);
-        }
-    }
-
-    private void onNetworkTunerChanged(Context context, boolean enabled) {
-        SharedPreferences sharedPreferences =
-                PreferenceManager.getDefaultSharedPreferences(context);
-        if (sharedPreferences.contains(PREFERENCE_IS_NETWORK_TUNER_ATTACHED)
-                && sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)
-                        == enabled) {
-            // the status is not changed
-            return;
-        }
-        if (enabled) {
-            sharedPreferences.edit().putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, true).apply();
-        } else {
-            sharedPreferences
-                    .edit()
-                    .putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)
-                    .apply();
-        }
-        // Network tuner detection is initiated by UI. So the app should not
-        // be killed.
-        handleTunerStatusChanged(
-                context, true, getConnectedUsbTuners(context), TunerHal.TUNER_TYPE_NETWORK);
-    }
-
-    /**
-     * See if any USB tuner hardware is attached in the system.
-     *
-     * @param context {@link Context} instance
-     * @return {@code true} if any tuner device we support is plugged in
-     */
-    private Set<TunerDevice> getConnectedUsbTuners(Context context) {
-        UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
-        Map<String, UsbDevice> deviceList = manager.getDeviceList();
-        String currentSecurityLevel =
-                SystemPropertiesProxy.getString(SECURITY_PATCH_LEVEL_KEY, null);
-
-        Set<TunerDevice> devices = new HashSet<>();
-        for (UsbDevice device : deviceList.values()) {
-            if (DEBUG) {
-                Log.d(TAG, "Device: " + device);
-            }
-            for (TunerDevice tuner : TUNER_DEVICES) {
-                if (tuner.equalsTo(device) && tuner.isSupported(currentSecurityLevel)) {
-                    Log.i(TAG, "Tuner found");
-                    devices.add(tuner);
-                }
-            }
-        }
-        return devices;
-    }
-
-    private void handleTunerStatusChanged(
-            Context context,
-            boolean forceDontKillApp,
-            Set<TunerDevice> connectedUsbTuners,
-            Integer triggerType) {
-        Map<ComponentName, Integer> serviceToEnable = new HashMap<>();
-        Set<ComponentName> serviceToDisable = new HashSet<>();
-        serviceToDisable.add(builtInTunerComponent);
-        serviceToDisable.add(networkTunerComponent);
-        if (TunerFeatures.TUNER.isEnabled(context)) {
-            // TODO: support both built-in tuner and other tuners at the same time?
-            if (TunerHal.useBuiltInTuner(context)) {
-                enableTunerTvInputService(
-                        context, true, false, TunerHal.TUNER_TYPE_BUILT_IN, builtInTunerComponent);
-                return;
-            }
-            SharedPreferences sharedPreferences =
-                    PreferenceManager.getDefaultSharedPreferences(context);
-            if (sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)) {
-                serviceToEnable.put(networkTunerComponent, TunerHal.TUNER_TYPE_NETWORK);
-            }
-        }
-        for (TunerDevice device : TUNER_DEVICES) {
-            if (TunerFeatures.TUNER.isEnabled(context) && connectedUsbTuners.contains(device)) {
-                serviceToEnable.put(mTunerServiceMapping.get(device), TunerHal.TUNER_TYPE_USB);
-            } else {
-                serviceToDisable.add(mTunerServiceMapping.get(device));
-            }
-        }
-        serviceToDisable.removeAll(serviceToEnable.keySet());
-        for (ComponentName serviceComponent : serviceToEnable.keySet()) {
-            if (isTunerPackageInstalled(context, serviceComponent)) {
-                enableTunerTvInputService(
-                        context,
-                        true,
-                        forceDontKillApp,
-                        serviceToEnable.get(serviceComponent),
-                        serviceComponent);
-            } else {
-                sendNotificationToInstallPackage(context, serviceComponent);
-            }
-        }
-        for (ComponentName serviceComponent : serviceToDisable) {
-            if (isTunerPackageInstalled(context, serviceComponent)) {
-                enableTunerTvInputService(
-                        context, false, forceDontKillApp, triggerType, serviceComponent);
-            } else {
-                cancelNotificationToInstallPackage(context, serviceComponent);
-            }
-        }
-    }
-
-    /**
-     * Enable/disable the component {@link BaseTunerTvInputService}.
-     *
-     * @param context {@link Context} instance
-     * @param enabled {@code true} to enable the service; otherwise {@code false}
-     */
-    private static void enableTunerTvInputService(
-            Context context,
-            boolean enabled,
-            boolean forceDontKillApp,
-            Integer tunerType,
-            ComponentName serviceComponent) {
-        if (DEBUG) Log.d(TAG, "enableTunerTvInputService: " + enabled);
-        PackageManager pm = context.getPackageManager();
-        int newState =
-                enabled
-                        ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
-                        : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
-        if (newState != pm.getComponentEnabledSetting(serviceComponent)) {
-            int flags = forceDontKillApp ? PackageManager.DONT_KILL_APP : 0;
-            if (serviceComponent.getPackageName().equals(context.getPackageName())) {
-                // Don't kill APP when handling input count changing. Or the following
-                // setComponentEnabledSetting() call won't work.
-                ((TvApplication) context.getApplicationContext())
-                        .handleInputCountChanged(true, enabled, true);
-                // Bundled input. Don't kill app if LiveChannels app is active since we don't want
-                // to kill the running app.
-                if (TvSingletons.getSingletons(context).getMainActivityWrapper().isCreated()) {
-                    flags |= PackageManager.DONT_KILL_APP;
-                }
-                // Send/cancel the USB tuner TV input setup notification.
-                BaseTunerSetupActivity.onTvInputEnabled(context, enabled, tunerType);
-                if (!enabled && tunerType != null) {
-                    if (tunerType == TunerHal.TUNER_TYPE_USB) {
-                        Toast.makeText(
-                                        context,
-                                        R.string.msg_usb_tuner_disconnected,
-                                        Toast.LENGTH_SHORT)
-                                .show();
-                    } else if (tunerType == TunerHal.TUNER_TYPE_NETWORK) {
-                        Toast.makeText(
-                                        context,
-                                        R.string.msg_network_tuner_disconnected,
-                                        Toast.LENGTH_SHORT)
-                                .show();
-                    }
-                }
-            }
-            // Enable/disable the USB tuner TV input.
-            pm.setComponentEnabledSetting(serviceComponent, newState, flags);
-            if (DEBUG) Log.d(TAG, "Status updated:" + enabled);
-        } else if (enabled && serviceComponent.getPackageName().equals(context.getPackageName())) {
-            // When # of tuners is changed or the tuner input service is switching from/to using
-            // network tuners or the device just boots.
-            TunerInputInfoUtils.updateTunerInputInfo(context);
-        }
-    }
-
-    /**
-     * Discovers a network tuner. If the network connection is down, it won't repeatedly checking.
-     */
-    public void executeNetworkTunerDiscoveryAsyncTask(final Context context) {
-        executeNetworkTunerDiscoveryAsyncTask(context, 0, 0);
-    }
-
-    /**
-     * Discovers a network tuner.
-     *
-     * @param context {@link Context}
-     * @param repeatedDurationMs The time length to wait to repeatedly check network status to start
-     *     finding network tuner when the network connection is not available. {@code 0} to disable
-     *     repeatedly checking.
-     * @param deviceIp The previous discovered device IP, 0 if none.
-     */
-    private void executeNetworkTunerDiscoveryAsyncTask(
-            final Context context, final long repeatedDurationMs, final int deviceIp) {
-        if (!TunerFeatures.NETWORK_TUNER.isEnabled(context)) {
-            return;
-        }
-        final Intent networkCheckingIntent = new Intent(context, IntentReceiver.class);
-        networkCheckingIntent.setAction(CHECKING_NETWORK_TUNER_STATUS);
-        if (!isNetworkConnected(context) && repeatedDurationMs > 0) {
-            sendCheckingAlarm(context, networkCheckingIntent, repeatedDurationMs);
-        } else {
-            new AsyncTask<Void, Void, Boolean>() {
-                @Override
-                protected Boolean doInBackground(Void... params) {
-                    Boolean result = null;
-                    // Implement and execute network tuner discovery AsyncTask here.
-                    return result;
-                }
-
-                @Override
-                protected void onPostExecute(Boolean foundNetworkTuner) {
-                    if (foundNetworkTuner == null) {
-                        return;
-                    }
-                    sendCheckingAlarm(
-                            context,
-                            networkCheckingIntent,
-                            foundNetworkTuner ? INITIAL_CHECKING_DURATION_MS : repeatedDurationMs);
-                    onNetworkTunerChanged(context, foundNetworkTuner);
-                }
-            }.execute();
-        }
-    }
-
-    private static boolean isNetworkConnected(Context context) {
-        ConnectivityManager cm =
-                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
-        NetworkInfo networkInfo = cm.getActiveNetworkInfo();
-        return networkInfo != null && networkInfo.isConnected();
-    }
-
-    private static void sendCheckingAlarm(Context context, Intent intent, long delayMs) {
-        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
-        intent.putExtra(EXTRA_CHECKING_DURATION, delayMs);
-        PendingIntent alarmIntent =
-                PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
-        alarmManager.set(
-                AlarmManager.ELAPSED_REALTIME,
-                SystemClock.elapsedRealtime() + delayMs,
-                alarmIntent);
-    }
-
-    private static boolean isTunerPackageInstalled(
-            Context context, ComponentName serviceComponent) {
-        try {
-            context.getPackageManager().getPackageInfo(serviceComponent.getPackageName(), 0);
-            return true;
-        } catch (NameNotFoundException e) {
-            return false;
-        }
-    }
-
-    private void sendNotificationToInstallPackage(Context context, ComponentName serviceComponent) {
-        if (!BuildConfig.ENG) {
-            return;
-        }
-        String applicationName = mTunerApplicationNames.get(serviceComponent);
-        if (applicationName == null) {
-            applicationName = context.getString(R.string.tuner_install_default_application_name);
-        }
-        String contentTitle =
-                context.getString(
-                        R.string.tuner_install_notification_content_title, applicationName);
-        String contentText = mNotificationMessages.get(serviceComponent);
-        if (contentText == null) {
-            contentText = context.getString(R.string.tuner_install_notification_content_text);
-        }
-        Bitmap largeIcon = mNotificationLargeIcons.get(serviceComponent);
-        if (largeIcon == null) {
-            // TODO: Make a better default image.
-            largeIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_store);
-        }
-        NotificationManager notificationManager =
-                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
-        if (notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) == null) {
-            createNotificationChannel(context, notificationManager);
-        }
-        Intent intent = new Intent(Intent.ACTION_VIEW);
-        intent.setData(
-                Uri.parse(
-                        String.format(
-                                PLAY_STORE_LINK_TEMPLATE, serviceComponent.getPackageName())));
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        Notification.Builder builder = new Notification.Builder(context, NOTIFICATION_CHANNEL_ID);
-        builder.setAutoCancel(true)
-                .setSmallIcon(R.drawable.ic_launcher_s)
-                .setLargeIcon(largeIcon)
-                .setContentTitle(contentTitle)
-                .setContentText(contentText)
-                .setCategory(Notification.CATEGORY_RECOMMENDATION)
-                .setContentIntent(PendingIntent.getActivity(context, 0, intent, 0));
-        notificationManager.notify(serviceComponent.getPackageName(), 0, builder.build());
-    }
-
-    private static void cancelNotificationToInstallPackage(
-            Context context, ComponentName serviceComponent) {
-        NotificationManager notificationManager =
-                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
-        notificationManager.cancel(serviceComponent.getPackageName(), 0);
-    }
-
-    private static void createNotificationChannel(
-            Context context, NotificationManager notificationManager) {
-        notificationManager.createNotificationChannel(
-                new NotificationChannel(
-                        NOTIFICATION_CHANNEL_ID,
-                        context.getResources()
-                                .getString(R.string.ut_setup_notification_channel_name),
-                        NotificationManager.IMPORTANCE_HIGH));
-    }
-
-    public static class IntentReceiver extends BroadcastReceiver {
-
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (DEBUG) Log.d(TAG, "Broadcast intent received:" + intent);
-            Starter.start(context);
-            TunerInputController tunerInputController =
-                    TvSingletons.getSingletons(context).getTunerInputController();
-            if (!TunerFeatures.TUNER.isEnabled(context)) {
-                tunerInputController.handleTunerStatusChanged(
-                        context, false, Collections.emptySet(), null);
-                return;
-            }
-            switch (intent.getAction()) {
-                case Intent.ACTION_BOOT_COMPLETED:
-                    tunerInputController.executeNetworkTunerDiscoveryAsyncTask(
-                            context, INITIAL_CHECKING_DURATION_MS, 0);
-                    // fall through
-                case TvApplication.ACTION_APPLICATION_FIRST_LAUNCHED:
-                case UsbManager.ACTION_USB_DEVICE_ATTACHED:
-                case UsbManager.ACTION_USB_DEVICE_DETACHED:
-                    tunerInputController.onCheckingUsbTunerStatus(context, intent.getAction());
-                    break;
-                case CHECKING_NETWORK_TUNER_STATUS:
-                    long repeatedDurationMs =
-                            intent.getLongExtra(
-                                    EXTRA_CHECKING_DURATION, INITIAL_CHECKING_DURATION_MS);
-                    tunerInputController.executeNetworkTunerDiscoveryAsyncTask(
-                            context,
-                            Math.min(repeatedDurationMs * 2, MAXIMUM_CHECKING_DURATION_MS),
-                            intent.getIntExtra(EXTRA_DEVICE_IP, 0));
-                    break;
-                default: // fall out
-            }
-        }
-    }
-
-    /**
-     * Simple data holder for a USB device. Used to represent a tuner model, and compare against
-     * {@link UsbDevice}.
-     */
-    private static class TunerDevice {
-        private final int vendorId;
-        private final int productId;
-
-        // security patch level from which the specific tuner type is supported.
-        private final String minSecurityLevel;
-
-        private TunerDevice(int vendorId, int productId, String minSecurityLevel) {
-            this.vendorId = vendorId;
-            this.productId = productId;
-            this.minSecurityLevel = minSecurityLevel;
-        }
-
-        private boolean equalsTo(UsbDevice device) {
-            return device.getVendorId() == vendorId && device.getProductId() == productId;
-        }
-
-        private boolean isSupported(String currentSecurityLevel) {
-            if (minSecurityLevel == null) {
-                return true;
-            }
-
-            long supportSecurityLevelTimeStamp = 0;
-            long currentSecurityLevelTimestamp = 0;
-            try {
-                SimpleDateFormat format = new SimpleDateFormat(SECURITY_PATCH_LEVEL_FORMAT);
-                supportSecurityLevelTimeStamp = format.parse(minSecurityLevel).getTime();
-                currentSecurityLevelTimestamp = format.parse(currentSecurityLevel).getTime();
-            } catch (ParseException e) {
-            }
-            return supportSecurityLevelTimeStamp != 0
-                    && supportSecurityLevelTimeStamp <= currentSecurityLevelTimestamp;
-        }
-    }
-
-    private static class CheckDvbDeviceHandler extends Handler {
-
-        private final TunerInputController mTunerInputController;
-        private DvbDeviceAccessor mDvbDeviceAccessor;
-
-        CheckDvbDeviceHandler(TunerInputController tunerInputController) {
-            super(Looper.getMainLooper());
-            this.mTunerInputController = tunerInputController;
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case MSG_ENABLE_INPUT_SERVICE:
-                    Context context = (Context) msg.obj;
-                    if (mDvbDeviceAccessor == null) {
-                        mDvbDeviceAccessor = new DvbDeviceAccessor(context);
-                    }
-                    boolean enabled = mDvbDeviceAccessor.isDvbDeviceAvailable();
-                    mTunerInputController.handleTunerStatusChanged(
-                            context,
-                            false,
-                            enabled
-                                    ? mTunerInputController.getConnectedUsbTuners(context)
-                                    : Collections.emptySet(),
-                            TunerHal.TUNER_TYPE_USB);
-                    break;
-                default: // fall out
-            }
-        }
-    }
-}
diff --git a/src/com/android/tv/util/Filter.java b/src/com/android/tv/tunerinputcontroller/BuiltInTunerManager.java
similarity index 62%
copy from src/com/android/tv/util/Filter.java
copy to src/com/android/tv/tunerinputcontroller/BuiltInTunerManager.java
index 3e24a49..e4fa35d 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/src/com/android/tv/tunerinputcontroller/BuiltInTunerManager.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,11 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package com.android.tv.tunerinputcontroller;
 
-package com.android.tv.util;
+import com.android.tv.common.singletons.HasTvInputId;
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
+/** Controllers and parameters needed to access a built in tuner. */
+public interface BuiltInTunerManager extends HasTvInputId {
+    TunerInputController getTunerInputController();
 }
diff --git a/src/com/android/tv/tunerinputcontroller/HasBuiltInTunerManager.java b/src/com/android/tv/tunerinputcontroller/HasBuiltInTunerManager.java
new file mode 100644
index 0000000..90540bc
--- /dev/null
+++ b/src/com/android/tv/tunerinputcontroller/HasBuiltInTunerManager.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tunerinputcontroller;
+
+import com.google.common.base.Optional;
+
+/**
+ * Has optional {@link BuiltInTunerManager}.
+ *
+ * <p>If the {@code BuiltInTunerManager} is absent the built tuner is not enabled.
+ */
+public interface HasBuiltInTunerManager {
+
+    /** @deprecated inject instead */
+    @Deprecated
+    Optional<BuiltInTunerManager> getBuiltInTunerManager();
+}
diff --git a/src/com/android/tv/tunerinputcontroller/TunerInputController.java b/src/com/android/tv/tunerinputcontroller/TunerInputController.java
new file mode 100644
index 0000000..f822dbe
--- /dev/null
+++ b/src/com/android/tv/tunerinputcontroller/TunerInputController.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tunerinputcontroller;
+
+import android.content.Context;
+import android.content.Intent;
+
+/** Controls the package visibility of built in tuner services. */
+public interface TunerInputController {
+
+    Intent createSetupIntent(Context context);
+
+    void onCheckingUsbTunerStatus(Context context, String action);
+
+    void executeNetworkTunerDiscoveryAsyncTask(Context context);
+
+    /**
+     * Updates tuner input's info.
+     *
+     * @param context {@link Context} instance
+     */
+    void updateTunerInputInfo(Context context);
+}
diff --git a/src/com/android/tv/ui/AppLayerTvView.java b/src/com/android/tv/ui/AppLayerTvView.java
index b2be9f0..e2b64a1 100644
--- a/src/com/android/tv/ui/AppLayerTvView.java
+++ b/src/com/android/tv/ui/AppLayerTvView.java
@@ -17,10 +17,10 @@
 package com.android.tv.ui;
 
 import android.content.Context;
-import android.media.tv.TvView;
 import android.util.AttributeSet;
 import android.view.SurfaceView;
 import android.view.View;
+import com.android.tv.common.compat.TvViewCompat;
 import com.android.tv.common.util.CommonUtils;
 import com.android.tv.common.util.Debug;
 
@@ -31,7 +31,7 @@
  * android.media.tv.TvView#setMain()} does not work because its implementation assumes that the app
  * uses only application layer. TODO: remove this class once the TvView.setMain() is revisited.
  */
-public class AppLayerTvView extends TvView {
+public class AppLayerTvView extends TvViewCompat {
     public AppLayerTvView(Context context) {
         super(context);
     }
diff --git a/src/com/android/tv/ui/BlockScreenView.java b/src/com/android/tv/ui/BlockScreenView.java
index 6b2d9a0..b7a2dd9 100644
--- a/src/com/android/tv/ui/BlockScreenView.java
+++ b/src/com/android/tv/ui/BlockScreenView.java
@@ -22,6 +22,7 @@
 import android.animation.AnimatorListenerAdapter;
 import android.content.Context;
 import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.View;
@@ -180,6 +181,10 @@
         requestLayout();
     }
 
+    public void setInfoTextOnClickListener(@Nullable OnClickListener onClickListener) {
+        mBlockingInfoTextView.setOnClickListener(onClickListener);
+    }
+
     /** Changes the view layout according to the {@code blockScreenType}. */
     public void onBlockStatusChanged(@BlockScreenType int blockScreenType, boolean withAnimation) {
         if (!withAnimation) {
@@ -252,4 +257,8 @@
             mInfoFadeOut.end();
         }
     }
+
+    public void setInfoTextClickable(boolean clickable) {
+        mBlockingInfoTextView.setClickable(clickable);
+    }
 }
diff --git a/src/com/android/tv/ui/ChannelBannerView.java b/src/com/android/tv/ui/ChannelBannerView.java
index 2832519..00ac7e3 100644
--- a/src/com/android/tv/ui/ChannelBannerView.java
+++ b/src/com/android/tv/ui/ChannelBannerView.java
@@ -46,11 +46,10 @@
 import android.widget.ProgressBar;
 import android.widget.RelativeLayout;
 import android.widget.TextView;
-import com.android.tv.MainActivity;
 import com.android.tv.R;
-import com.android.tv.TvSingletons;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.singletons.HasSingletons;
 import com.android.tv.data.Program;
 import com.android.tv.data.StreamInfo;
 import com.android.tv.data.api.Channel;
@@ -59,11 +58,14 @@
 import com.android.tv.parental.ContentRatingsManager;
 import com.android.tv.ui.TvTransitionManager.TransitionLayout;
 import com.android.tv.ui.hideable.AutoHideScheduler;
+import com.android.tv.util.TvInputManagerHelper;
 import com.android.tv.util.Utils;
 import com.android.tv.util.images.ImageCache;
 import com.android.tv.util.images.ImageLoader;
 import com.android.tv.util.images.ImageLoader.ImageLoaderCallback;
 import com.android.tv.util.images.ImageLoader.LoadTvInputLogoTask;
+import com.google.common.collect.ImmutableList;
+import javax.inject.Provider;
 
 /** A view to render channel banner. */
 public class ChannelBannerView extends FrameLayout
@@ -74,6 +76,21 @@
     /** Show all information at the channel banner. */
     public static final int LOCK_NONE = 0;
 
+    /** Singletons needed for this class. */
+    public interface MySingletons {
+        Provider<Channel> getCurrentChannelProvider();
+
+        Provider<Program> getCurrentProgramProvider();
+
+        Provider<TvOverlayManager> getOverlayManagerProvider();
+
+        TvInputManagerHelper getTvInputManagerHelperSingleton();
+
+        Provider<Long> getCurrentPlayingPositionProvider();
+
+        DvrManager getDvrManagerSingleton();
+    }
+
     /**
      * Lock program details at the channel banner. This is used when a content is locked so we don't
      * want to show program details including program description text and poster art.
@@ -94,14 +111,21 @@
     private Program mLockedChannelProgram;
     private static String sClosedCaptionMark;
 
-    private final MainActivity mMainActivity;
     private final Resources mResources;
+    private final Provider<Channel> mCurrentChannelProvider;
+    private final Provider<Program> mCurrentProgramProvider;
+    private final Provider<Long> mCurrentPlayingPositionProvider;
+    private final TvInputManagerHelper mTvInputManagerHelper;
+    // TvOverlayManager is always created after ChannelBannerView
+    private final Provider<TvOverlayManager> mTvOverlayManager;
+
     private View mChannelView;
 
     private TextView mChannelNumberTextView;
     private ImageView mChannelLogoImageView;
     private TextView mProgramTextView;
     private ImageView mTvInputLogoImageView;
+    private ImageView mChannelSignalStrengthView;
     private TextView mChannelNameTextView;
     private TextView mProgramTimeTextView;
     private ProgressBar mRemainingTimeView;
@@ -143,6 +167,32 @@
     private final int mRecordingIconPadding;
     private final Interpolator mResizeInterpolator;
 
+    /**
+     * 0 - 100 represent signal strength percentage. Strength is divided into 5 levels (0 - 4).
+     *
+     * <p>This is the upper boundary of level 0 [0%, 20%], and the lower boundary of level 1 (20%,
+     * 40%].
+     */
+    private static final int SIGNAL_STRENGTH_0_OF_4_UPPER_BOUND = 20;
+
+    /**
+     * This is the upper boundary of level 1 (20%, 40%], and the lower boundary of level 2 (40%,
+     * 60%].
+     */
+    private static final int SIGNAL_STRENGTH_1_OF_4_UPPER_BOUND = 40;
+
+    /**
+     * This is the upper boundary of level of level 2. (40%, 60%], and the lower boundary of level 3
+     * (60%, 80%].
+     */
+    private static final int SIGNAL_STRENGTH_2_OF_4_UPPER_BOUND = 60;
+
+    /**
+     * This is the upper boundary of level of level 3 (60%, 80%], and the lower boundary of level 4
+     * (80%, 100%].
+     */
+    private static final int SIGNAL_STRENGTH_3_OF_4_UPPER_BOUND = 80;
+
     private final AnimatorListenerAdapter mResizeAnimatorListener =
             new AnimatorListenerAdapter() {
                 @Override
@@ -172,7 +222,14 @@
     public ChannelBannerView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
         mResources = getResources();
-        mMainActivity = (MainActivity) context;
+
+        @SuppressWarnings("unchecked") // injection
+        MySingletons singletons = HasSingletons.get(MySingletons.class, context);
+        mCurrentChannelProvider = singletons.getCurrentChannelProvider();
+        mCurrentProgramProvider = singletons.getCurrentProgramProvider();
+        mCurrentPlayingPositionProvider = singletons.getCurrentPlayingPositionProvider();
+        mTvInputManagerHelper = singletons.getTvInputManagerHelperSingleton();
+        mTvOverlayManager = singletons.getOverlayManagerProvider();
 
         mShowDurationMillis = mResources.getInteger(R.integer.channel_banner_show_duration);
         mChannelLogoImageViewWidth =
@@ -195,20 +252,17 @@
 
         mProgramDescriptionFadeInAnimator =
                 AnimatorInflater.loadAnimator(
-                        mMainActivity, R.animator.channel_banner_program_description_fade_in);
+                        context, R.animator.channel_banner_program_description_fade_in);
         mProgramDescriptionFadeOutAnimator =
                 AnimatorInflater.loadAnimator(
-                        mMainActivity, R.animator.channel_banner_program_description_fade_out);
+                        context, R.animator.channel_banner_program_description_fade_out);
 
-        if (CommonFeatures.DVR.isEnabled(mMainActivity)) {
-            mDvrManager = TvSingletons.getSingletons(mMainActivity).getDvrManager();
+        if (CommonFeatures.DVR.isEnabled(context)) {
+            mDvrManager = singletons.getDvrManagerSingleton();
         } else {
             mDvrManager = null;
         }
-        mContentRatingsManager =
-                TvSingletons.getSingletons(getContext())
-                        .getTvInputManagerHelper()
-                        .getContentRatingsManager();
+        mContentRatingsManager = mTvInputManagerHelper.getContentRatingsManager();
 
         mNoProgram =
                 new Program.Builder()
@@ -234,22 +288,23 @@
 
         mChannelView = findViewById(R.id.channel_banner_view);
 
-        mChannelNumberTextView = (TextView) findViewById(R.id.channel_number);
-        mChannelLogoImageView = (ImageView) findViewById(R.id.channel_logo);
-        mProgramTextView = (TextView) findViewById(R.id.program_text);
-        mTvInputLogoImageView = (ImageView) findViewById(R.id.tvinput_logo);
-        mChannelNameTextView = (TextView) findViewById(R.id.channel_name);
-        mProgramTimeTextView = (TextView) findViewById(R.id.program_time_text);
-        mRemainingTimeView = (ProgressBar) findViewById(R.id.remaining_time);
-        mRecordingIndicatorView = (TextView) findViewById(R.id.recording_indicator);
-        mClosedCaptionTextView = (TextView) findViewById(R.id.closed_caption);
-        mAspectRatioTextView = (TextView) findViewById(R.id.aspect_ratio);
-        mResolutionTextView = (TextView) findViewById(R.id.resolution);
-        mAudioChannelTextView = (TextView) findViewById(R.id.audio_channel);
-        mContentRatingsTextViews[0] = (TextView) findViewById(R.id.content_ratings_0);
-        mContentRatingsTextViews[1] = (TextView) findViewById(R.id.content_ratings_1);
-        mContentRatingsTextViews[2] = (TextView) findViewById(R.id.content_ratings_2);
-        mProgramDescriptionTextView = (TextView) findViewById(R.id.program_description);
+        mChannelNumberTextView = findViewById(R.id.channel_number);
+        mChannelLogoImageView = findViewById(R.id.channel_logo);
+        mProgramTextView = findViewById(R.id.program_text);
+        mTvInputLogoImageView = findViewById(R.id.tvinput_logo);
+        mChannelSignalStrengthView = findViewById(R.id.channel_signal_strength);
+        mChannelNameTextView = findViewById(R.id.channel_name);
+        mProgramTimeTextView = findViewById(R.id.program_time_text);
+        mRemainingTimeView = findViewById(R.id.remaining_time);
+        mRecordingIndicatorView = findViewById(R.id.recording_indicator);
+        mClosedCaptionTextView = findViewById(R.id.closed_caption);
+        mAspectRatioTextView = findViewById(R.id.aspect_ratio);
+        mResolutionTextView = findViewById(R.id.resolution);
+        mAudioChannelTextView = findViewById(R.id.audio_channel);
+        mContentRatingsTextViews[0] = findViewById(R.id.content_ratings_0);
+        mContentRatingsTextViews[1] = findViewById(R.id.content_ratings_1);
+        mContentRatingsTextViews[2] = findViewById(R.id.content_ratings_2);
+        mProgramDescriptionTextView = findViewById(R.id.program_description);
         mAnchorView = findViewById(R.id.anchor);
 
         mProgramDescriptionFadeInAnimator.setTarget(mProgramDescriptionTextView);
@@ -310,7 +365,7 @@
      */
     public void setBlockingContentRating(TvContentRating rating) {
         mBlockingContentRating = rating;
-        updateProgramRatings(mMainActivity.getCurrentProgram());
+        updateProgramRatings(mCurrentProgramProvider.get());
     }
 
     /**
@@ -328,20 +383,20 @@
                 mAutoHideScheduler.schedule(mShowDurationMillis);
             }
             mBlockingContentRating = null;
-            mCurrentChannel = mMainActivity.getCurrentChannel();
+            mCurrentChannel = mCurrentChannelProvider.get();
             mCurrentChannelLogoExists =
                     mCurrentChannel != null && mCurrentChannel.channelLogoExists();
             updateStreamInfo(null);
             updateChannelInfo();
         }
-        updateProgramInfo(mMainActivity.getCurrentProgram());
+        updateProgramInfo(mCurrentProgramProvider.get());
         mUpdateOnTune = false;
     }
 
     private void hide() {
         mCurrentHeight = 0;
-        mMainActivity
-                .getOverlayManager()
+        mTvOverlayManager
+                .get()
                 .hideOverlays(
                         TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG
                                 | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS
@@ -367,10 +422,10 @@
             updateText(
                     mResolutionTextView,
                     Utils.getVideoDefinitionLevelString(
-                            mMainActivity, info.getVideoDefinitionLevel()));
+                            getContext(), info.getVideoDefinitionLevel()));
             updateText(
                     mAudioChannelTextView,
-                    Utils.getAudioChannelString(mMainActivity, info.getAudioChannelCount()));
+                    Utils.getAudioChannelString(getContext(), info.getAudioChannelCount()));
         } else {
             // Channel change has been requested. But, StreamInfo hasn't been updated yet.
             mClosedCaptionTextView.setVisibility(View.GONE);
@@ -418,8 +473,7 @@
         }
         mChannelNumberTextView.setText(displayNumber);
         mChannelNameTextView.setText(displayName);
-        TvInputInfo info =
-                mMainActivity.getTvInputManagerHelper().getTvInputInfo(getCurrentInputId());
+        TvInputInfo info = mTvInputManagerHelper.getTvInputInfo(getCurrentInputId());
         if (info == null
                 || !ImageLoader.loadBitmap(
                         createTvInputLogoLoaderCallback(info, this),
@@ -440,7 +494,7 @@
     }
 
     private String getCurrentInputId() {
-        Channel channel = mMainActivity.getCurrentChannel();
+        Channel channel = mCurrentChannelProvider.get();
         return channel != null ? channel.getInputId() : null;
     }
 
@@ -503,6 +557,34 @@
         };
     }
 
+    public void updateChannelSignalStrengthView(int value) {
+        int resId = signalStrenghtToResId(value);
+        if (resId != 0) {
+            mChannelSignalStrengthView.setVisibility(View.VISIBLE);
+            mChannelSignalStrengthView.setImageResource(resId);
+        } else {
+            mChannelSignalStrengthView.setVisibility(View.GONE);
+        }
+    }
+
+    private int signalStrenghtToResId(int value) {
+        int signal = 0;
+        if (value >= 0 && value <= 100) {
+            if (value <= SIGNAL_STRENGTH_0_OF_4_UPPER_BOUND) {
+                signal = R.drawable.quantum_ic_signal_cellular_0_bar_white_24;
+            } else if (value <= SIGNAL_STRENGTH_1_OF_4_UPPER_BOUND) {
+                signal = R.drawable.quantum_ic_signal_cellular_1_bar_white_24;
+            } else if (value <= SIGNAL_STRENGTH_2_OF_4_UPPER_BOUND) {
+                signal = R.drawable.quantum_ic_signal_cellular_2_bar_white_24;
+            } else if (value <= SIGNAL_STRENGTH_3_OF_4_UPPER_BOUND) {
+                signal = R.drawable.quantum_ic_signal_cellular_3_bar_white_24;
+            } else {
+                signal = R.drawable.quantum_ic_signal_cellular_4_bar_white_24;
+            }
+        }
+        return signal;
+    }
+
     private void updateLogo(@Nullable Bitmap logo) {
         if (logo == null) {
             // Need to update the text size of the program text view depending on the channel logo.
@@ -651,13 +733,14 @@
                 mContentRatingsTextViews[i].setVisibility(View.GONE);
             }
         } else {
-            TvContentRating[] ratings = (program == null) ? null : program.getContentRatings();
+            ImmutableList<TvContentRating> ratings =
+                    (program == null) ? null : program.getContentRatings();
             for (int i = 0; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) {
-                if (ratings == null || ratings.length <= i) {
+                if (ratings == null || ratings.size() <= i) {
                     mContentRatingsTextViews[i].setVisibility(View.GONE);
                 } else {
                     mContentRatingsTextViews[i].setText(
-                            mContentRatingsManager.getDisplayNameForRating(ratings[i]));
+                            mContentRatingsManager.getDisplayNameForRating(ratings.get(i)));
                     mContentRatingsTextViews[i].setVisibility(View.VISIBLE);
                 }
             }
@@ -667,13 +750,11 @@
     private void updateProgramTimeInfo(Program program) {
         long durationMs = program.getDurationMillis();
         long startTimeMs = program.getStartTimeUtcMillis();
-        long endTimeMs = program.getEndTimeUtcMillis();
 
         if (mLockType != LOCK_CHANNEL_INFO && durationMs > 0 && startTimeMs > 0) {
             mProgramTimeTextView.setVisibility(View.VISIBLE);
             mRemainingTimeView.setVisibility(View.VISIBLE);
-            mProgramTimeTextView.setText(
-                    Utils.getDurationString(getContext(), startTimeMs, endTimeMs, true));
+            mProgramTimeTextView.setText(program.getDurationString(getContext()));
         } else {
             mProgramTimeTextView.setVisibility(View.GONE);
             mRemainingTimeView.setVisibility(View.GONE);
@@ -713,7 +794,7 @@
             Program program, @Nullable ScheduledRecording recording) {
         long programStartTime = program.getStartTimeUtcMillis();
         long programEndTime = program.getEndTimeUtcMillis();
-        long currentPosition = mMainActivity.getCurrentPlayingPosition();
+        long currentPosition = mCurrentPlayingPositionProvider.get();
         updateRecordingIndicator(recording);
         if (recording != null) {
             // Recording now. Use recording-style progress bar.
@@ -734,12 +815,12 @@
         if (recording != null) {
             if (mRemainingTimeView.getVisibility() == View.GONE) {
                 mRecordingIndicatorView.setText(
-                        mMainActivity
+                        getContext()
                                 .getResources()
                                 .getString(
                                         R.string.dvr_recording_till_format,
                                         DateUtils.formatDateTime(
-                                                mMainActivity,
+                                                getContext(),
                                                 recording.getEndTimeMs(),
                                                 DateUtils.FORMAT_SHOW_TIME)));
                 mRecordingIndicatorView.setCompoundDrawablePadding(mRecordingIconPadding);
@@ -754,7 +835,7 @@
     }
 
     private boolean isCurrentProgram(ScheduledRecording recording, Program program) {
-        long currentPosition = mMainActivity.getCurrentPlayingPosition();
+        long currentPosition = mCurrentPlayingPositionProvider.get();
         return (recording.getType() == ScheduledRecording.TYPE_PROGRAM
                         && recording.getProgramId() == program.getId())
                 || (recording.getType() == ScheduledRecording.TYPE_TIMED
diff --git a/src/com/android/tv/ui/DetailsActivity.java b/src/com/android/tv/ui/DetailsActivity.java
new file mode 100644
index 0000000..80c0f64
--- /dev/null
+++ b/src/com/android/tv/ui/DetailsActivity.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.ui;
+
+import android.app.Activity;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.annotation.NonNull;
+import android.support.v17.leanback.app.DetailsFragment;
+import android.transition.Transition;
+import android.transition.Transition.TransitionListener;
+import android.util.Log;
+import android.view.View;
+import com.android.tv.R;
+import com.android.tv.Starter;
+import com.android.tv.TvSingletons;
+import com.android.tv.dialog.PinDialogFragment;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.ui.browse.CurrentRecordingDetailsFragment;
+import com.android.tv.dvr.ui.browse.RecordedProgramDetailsFragment;
+import com.android.tv.dvr.ui.browse.ScheduledRecordingDetailsFragment;
+import com.android.tv.dvr.ui.browse.SeriesRecordingDetailsFragment;
+
+/** Activity to show details view. */
+public class DetailsActivity extends Activity implements PinDialogFragment.OnPinCheckedListener {
+    private static final String TAG = "DetailsActivity";
+
+    private static final long INVALID_RECORD_ID = -1;
+
+    /** Name of record id added to the Intent. */
+    public static final String RECORDING_ID = "record_id";
+    /** Name of program uri added to the Intent. */
+    public static final String PROGRAM = "program";
+    /** Name of channel id added to the Intent. */
+    public static final String CHANNEL_ID = "channel_id";
+    /** Name of input id added to the Intent. */
+    public static final String INPUT_ID = "input_id";
+
+    /**
+     * Name of flag added to the Intent to determine if details view should hide "View schedule"
+     * button.
+     */
+    public static final String HIDE_VIEW_SCHEDULE = "hide_view_schedule";
+
+    /** Name of details view's type added to the intent. */
+    public static final String DETAILS_VIEW_TYPE = "details_view_type";
+
+    /** Name of shared element between activities. */
+    public static final String SHARED_ELEMENT_NAME = "shared_element";
+
+    /** CURRENT_RECORDING_VIEW refers to Current Recordings in DVR. */
+    public static final int CURRENT_RECORDING_VIEW = 1;
+
+    /** SCHEDULED_RECORDING_VIEW refers to Scheduled Recordings in DVR. */
+    public static final int SCHEDULED_RECORDING_VIEW = 2;
+
+    /** RECORDED_PROGRAM_VIEW refers to Recorded programs in DVR. */
+    public static final int RECORDED_PROGRAM_VIEW = 3;
+
+    /** SERIES_RECORDING_VIEW refers to series recording in DVR. */
+    public static final int SERIES_RECORDING_VIEW = 4;
+
+    /** SERIES_RECORDING_VIEW refers to program. */
+    public static final int PROGRAM_VIEW = 5;
+
+    public static final int REQUEST_DELETE = 1;
+
+    private PinDialogFragment.OnPinCheckedListener mOnPinCheckedListener;
+    private long mRecordId = INVALID_RECORD_ID;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        Starter.start(this);
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_dvr_details);
+        long recordId = getIntent().getLongExtra(RECORDING_ID, INVALID_RECORD_ID);
+        int detailsViewType = getIntent().getIntExtra(DETAILS_VIEW_TYPE, -1);
+        boolean hideViewSchedule = getIntent().getBooleanExtra(HIDE_VIEW_SCHEDULE, false);
+        long channelId = getIntent().getLongExtra(CHANNEL_ID, -1);
+        DetailsFragment detailsFragment = null;
+        Bundle args = new Bundle();
+        if (detailsViewType != -1 && savedInstanceState == null) {
+            if (recordId != INVALID_RECORD_ID) {
+                mRecordId = recordId;
+                args.putLong(RECORDING_ID, mRecordId);
+                if (detailsViewType == CURRENT_RECORDING_VIEW) {
+                    detailsFragment = new CurrentRecordingDetailsFragment();
+                } else if (detailsViewType == SCHEDULED_RECORDING_VIEW) {
+                    args.putBoolean(HIDE_VIEW_SCHEDULE, hideViewSchedule);
+                    detailsFragment = new ScheduledRecordingDetailsFragment();
+                } else if (detailsViewType == RECORDED_PROGRAM_VIEW) {
+                    detailsFragment = new RecordedProgramDetailsFragment();
+                } else if (detailsViewType == SERIES_RECORDING_VIEW) {
+                    detailsFragment = new SeriesRecordingDetailsFragment();
+                }
+            } else if (detailsViewType == PROGRAM_VIEW && channelId != -1) {
+                Parcelable program = getIntent().getParcelableExtra(PROGRAM);
+                if (program != null) {
+                    args.putLong(CHANNEL_ID, channelId);
+                    args.putParcelable(PROGRAM, program);
+                    args.putString(INPUT_ID, getIntent().getStringExtra(INPUT_ID));
+                    detailsFragment = new ProgramDetailsFragment();
+                }
+            }
+            if (detailsFragment != null) {
+                detailsFragment.setArguments(args);
+                getFragmentManager()
+                        .beginTransaction()
+                        .replace(R.id.dvr_details_view_frame, detailsFragment)
+                        .commit();
+            }
+        }
+
+        // This is a workaround for the focus on O device
+        addTransitionListener();
+    }
+
+    @Override
+    public void onPinChecked(boolean checked, int type, String rating) {
+        if (mOnPinCheckedListener != null) {
+            mOnPinCheckedListener.onPinChecked(checked, type, rating);
+        }
+    }
+
+    public void setOnPinCheckListener(PinDialogFragment.OnPinCheckedListener listener) {
+        mOnPinCheckedListener = listener;
+    }
+
+    private void addTransitionListener() {
+        getWindow()
+                .getSharedElementEnterTransition()
+                .addListener(
+                        new TransitionListener() {
+                            @Override
+                            public void onTransitionStart(Transition transition) {
+                                // Do nothing
+                            }
+
+                            @Override
+                            public void onTransitionEnd(Transition transition) {
+                                View actions = findViewById(R.id.details_overview_actions);
+                                if (actions != null) {
+                                    actions.requestFocus();
+                                }
+                            }
+
+                            @Override
+                            public void onTransitionCancel(Transition transition) {
+                                // Do nothing
+
+                            }
+
+                            @Override
+                            public void onTransitionPause(Transition transition) {
+                                // Do nothing
+                            }
+
+                            @Override
+                            public void onTransitionResume(Transition transition) {
+                                // Do nothing
+                            }
+                        });
+    }
+
+    @Override
+    public void onRequestPermissionsResult(
+            int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+        switch (requestCode) {
+            case REQUEST_DELETE:
+                // If request is cancelled, the result arrays are empty.
+                if (grantResults.length > 0
+                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+                    delete(true);
+                } else {
+                    Log.i(
+                            TAG,
+                            "Write permission denied, Not trying to delete the file for "
+                                    + mRecordId);
+                    delete(false);
+                }
+                break;
+            default:
+                super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+        }
+    }
+
+    private void delete(boolean deleteFile) {
+        if (mRecordId != INVALID_RECORD_ID) {
+            DvrManager dvrManager = TvSingletons.getSingletons(this).getDvrManager();
+            dvrManager.removeRecordedProgram(mRecordId, deleteFile);
+        }
+        finish();
+    }
+}
diff --git a/src/com/android/tv/ui/FullscreenDialogView.java b/src/com/android/tv/ui/FullscreenDialogView.java
index 800fa85..d3fec82 100644
--- a/src/com/android/tv/ui/FullscreenDialogView.java
+++ b/src/com/android/tv/ui/FullscreenDialogView.java
@@ -83,13 +83,7 @@
 
     /** Dismisses the host {@link Dialog}. */
     protected void dismiss() {
-        startExitAnimation(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        mDialog.dismiss();
-                    }
-                });
+        startExitAnimation(() -> mDialog.dismiss());
     }
 
     @Override
@@ -110,9 +104,7 @@
         v.mSkipEnterAlphaAnimation = true;
         v.initialize(mActivity, mDialog);
         startExitAnimation(
-                new Runnable() {
-                    @Override
-                    public void run() {
+                () ->
                         new Handler()
                                 .postDelayed(
                                         new Runnable() {
@@ -122,9 +114,7 @@
                                                 getDialog().setContentView(v);
                                             }
                                         },
-                                        TRANSITION_INTERVAL_MS);
-                    }
-                });
+                                        TRANSITION_INTERVAL_MS));
     }
 
     /** Called when an enter animation starts. Sub-view specific animation can be implemented. */
diff --git a/src/com/android/tv/ui/InputBannerView.java b/src/com/android/tv/ui/InputBannerView.java
index 5ac715b..d060918 100644
--- a/src/com/android/tv/ui/InputBannerView.java
+++ b/src/com/android/tv/ui/InputBannerView.java
@@ -31,9 +31,7 @@
     private final long mShowDurationMillis;
 
     private final Runnable mHideRunnable =
-            new Runnable() {
-                @Override
-                public void run() {
+            () ->
                     ((MainActivity) getContext())
                             .getOverlayManager()
                             .hideOverlays(
@@ -42,9 +40,6 @@
                                             | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE
                                             | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU
                                             | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
-                }
-            };
-
     private TextView mInputLabelTextView;
     private TextView mSecondaryInputLabelTextView;
 
diff --git a/src/com/android/tv/ui/IntroView.java b/src/com/android/tv/ui/IntroView.java
index be9fb69..e724074 100644
--- a/src/com/android/tv/ui/IntroView.java
+++ b/src/com/android/tv/ui/IntroView.java
@@ -102,13 +102,7 @@
                 .setInterpolator(interpolator)
                 .setDuration(duration)
                 .withLayer()
-                .withEndAction(
-                        new Runnable() {
-                            @Override
-                            public void run() {
-                                onAnimationEnded.run();
-                            }
-                        })
+                .withEndAction(onAnimationEnded)
                 .start();
     }
 }
diff --git a/src/com/android/tv/ui/KeypadChannelSwitchView.java b/src/com/android/tv/ui/KeypadChannelSwitchView.java
index e262581..a26175a 100644
--- a/src/com/android/tv/ui/KeypadChannelSwitchView.java
+++ b/src/com/android/tv/ui/KeypadChannelSwitchView.java
@@ -148,13 +148,10 @@
                         mChannelItemListView.setFocusable(false);
                         final Channel channel = ((Channel) mAdapter.getItem(position));
                         postDelayed(
-                                new Runnable() {
-                                    @Override
-                                    public void run() {
-                                        mChannelItemListView.setFocusable(true);
-                                        mMainActivity.tuneToChannel(channel);
-                                        mTracker.sendChannelNumberItemClicked();
-                                    }
+                                () -> {
+                                    mChannelItemListView.setFocusable(true);
+                                    mMainActivity.tuneToChannel(channel);
+                                    mTracker.sendChannelNumberItemClicked();
                                 },
                                 mRippleAnimDurationMillis);
                     }
diff --git a/src/com/android/tv/ui/ProgramDetailsFragment.java b/src/com/android/tv/ui/ProgramDetailsFragment.java
new file mode 100644
index 0000000..88a7b2c
--- /dev/null
+++ b/src/com/android/tv/ui/ProgramDetailsFragment.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.ui;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v17.leanback.app.DetailsFragment;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.ClassPresenterSelector;
+import android.support.v17.leanback.widget.DetailsOverviewRow;
+import android.support.v17.leanback.widget.DetailsOverviewRowPresenter;
+import android.support.v17.leanback.widget.OnActionClickedListener;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.text.TextUtils;
+import com.android.tv.R;
+import com.android.tv.TvSingletons;
+import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.data.Program;
+import com.android.tv.data.api.Channel;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.DvrScheduleManager;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.ui.DvrUiHelper;
+import com.android.tv.dvr.ui.browse.ActionPresenterSelector;
+import com.android.tv.dvr.ui.browse.DetailsContent;
+import com.android.tv.dvr.ui.browse.DetailsContentPresenter;
+import com.android.tv.dvr.ui.browse.DetailsViewBackgroundHelper;
+import com.android.tv.util.images.ImageLoader;
+
+/** A fragment shows the details of a Program */
+public class ProgramDetailsFragment extends DetailsFragment
+        implements DvrDataManager.ScheduledRecordingListener,
+                DvrScheduleManager.OnConflictStateChangeListener {
+    private static final int LOAD_LOGO_IMAGE = 1;
+    private static final int LOAD_BACKGROUND_IMAGE = 2;
+
+    private static final int ACTION_VIEW_SCHEDULE = 1;
+    private static final int ACTION_CANCEL = 2;
+    private static final int ACTION_SCHEDULE_RECORDING = 3;
+
+    protected DetailsViewBackgroundHelper mBackgroundHelper;
+    private ArrayObjectAdapter mRowsAdapter;
+    private DetailsOverviewRow mDetailsOverview;
+    private Program mProgram;
+    private String mInputId;
+    private ScheduledRecording mScheduledRecording;
+    private DvrManager mDvrManager;
+    private DvrDataManager mDvrDataManager;
+    private DvrScheduleManager mDvrScheduleManager;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        if (!onLoadDetails(getArguments())) {
+            getActivity().finish();
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        mDvrDataManager.removeScheduledRecordingListener(this);
+        mDvrScheduleManager.removeOnConflictStateChangeListener(this);
+        super.onDestroy();
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        VerticalGridView container =
+                (VerticalGridView) getActivity().findViewById(R.id.container_list);
+        // Need to manually modify offset. Please refer DetailsFragment.setVerticalGridViewLayout.
+        container.setItemAlignmentOffset(0);
+        container.setWindowAlignmentOffset(
+                getResources().getDimensionPixelSize(R.dimen.lb_details_rows_align_top));
+    }
+
+    private void setupAdapter() {
+        DetailsOverviewRowPresenter rowPresenter =
+                new DetailsOverviewRowPresenter(new DetailsContentPresenter(getActivity()));
+        rowPresenter.setBackgroundColor(
+                getResources().getColor(R.color.common_tv_background, null));
+        rowPresenter.setSharedElementEnterTransition(
+                getActivity(), DetailsActivity.SHARED_ELEMENT_NAME);
+        rowPresenter.setOnActionClickedListener(onCreateOnActionClickedListener());
+        mRowsAdapter = new ArrayObjectAdapter(onCreatePresenterSelector(rowPresenter));
+        setAdapter(mRowsAdapter);
+    }
+
+    /** Sets details overview. */
+    protected void setDetailsOverviewRow(DetailsContent detailsContent) {
+        mDetailsOverview = new DetailsOverviewRow(detailsContent);
+        mDetailsOverview.setActionsAdapter(onCreateActionsAdapter());
+        mRowsAdapter.add(mDetailsOverview);
+        onLoadLogoAndBackgroundImages(detailsContent);
+    }
+
+    /** Creates and returns presenter selector will be used by rows adaptor. */
+    protected PresenterSelector onCreatePresenterSelector(
+            DetailsOverviewRowPresenter rowPresenter) {
+        ClassPresenterSelector presenterSelector = new ClassPresenterSelector();
+        presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter);
+        return presenterSelector;
+    }
+
+    /** Updates actions of details overview. */
+    protected void updateActions() {
+        mDetailsOverview.setActionsAdapter(onCreateActionsAdapter());
+    }
+
+    /**
+     * Loads program details according to the arguments the fragment got.
+     *
+     * @return false if cannot find valid programs, else return true. If the return value is false,
+     *     the detail activity and fragment will be ended.
+     */
+    private boolean onLoadDetails(Bundle args) {
+        Program program = args.getParcelable(DetailsActivity.PROGRAM);
+        long channelId = args.getLong(DetailsActivity.CHANNEL_ID);
+        String inputId = args.getString(DetailsActivity.INPUT_ID);
+        if (program != null && channelId != Channel.INVALID_ID && !TextUtils.isEmpty(inputId)) {
+            mProgram = program;
+            mInputId = inputId;
+            TvSingletons singletons = TvSingletons.getSingletons(getContext());
+            mDvrDataManager = singletons.getDvrDataManager();
+            mDvrManager = singletons.getDvrManager();
+            mDvrScheduleManager = singletons.getDvrScheduleManager();
+            mScheduledRecording =
+                    mDvrDataManager.getScheduledRecordingForProgramId(program.getId());
+            mBackgroundHelper = new DetailsViewBackgroundHelper(getActivity());
+            setupAdapter();
+            setDetailsOverviewRow(DetailsContent.createFromProgram(getContext(), mProgram));
+            mDvrDataManager.addScheduledRecordingListener(this);
+            mDvrScheduleManager.addOnConflictStateChangeListener(this);
+            return true;
+        }
+        return false;
+    }
+
+    private int getScheduleIconId() {
+        if (mDvrManager.isConflicting(mScheduledRecording)) {
+            return R.drawable.ic_warning_white_32dp;
+        } else {
+            return R.drawable.ic_schedule_32dp;
+        }
+    }
+
+    /** Creates actions users can interact with and their adaptor for this fragment. */
+    private SparseArrayObjectAdapter onCreateActionsAdapter() {
+        SparseArrayObjectAdapter adapter =
+                new SparseArrayObjectAdapter(new ActionPresenterSelector());
+        Resources res = getResources();
+        if (mScheduledRecording != null) {
+            adapter.set(
+                    ACTION_VIEW_SCHEDULE,
+                    new Action(
+                            ACTION_VIEW_SCHEDULE,
+                            res.getString(R.string.dvr_detail_view_schedule),
+                            null,
+                            res.getDrawable(getScheduleIconId())));
+            adapter.set(
+                    ACTION_CANCEL,
+                    new Action(
+                            ACTION_CANCEL,
+                            res.getString(R.string.dvr_detail_cancel_recording),
+                            null,
+                            res.getDrawable(R.drawable.ic_dvr_cancel_32dp)));
+        } else if (CommonFeatures.DVR.isEnabled(getActivity())
+                && mDvrManager.isProgramRecordable(mProgram)) {
+            adapter.set(
+                    ACTION_SCHEDULE_RECORDING,
+                    new Action(
+                            ACTION_SCHEDULE_RECORDING,
+                            res.getString(R.string.dvr_detail_schedule_recording),
+                            null,
+                            res.getDrawable(R.drawable.ic_schedule_32dp)));
+        }
+        return adapter;
+    }
+
+    /**
+     * Creates actions listeners to implement the behavior of the fragment after users click some
+     * action buttons.
+     */
+    private OnActionClickedListener onCreateOnActionClickedListener() {
+        return new OnActionClickedListener() {
+            @Override
+            public void onActionClicked(Action action) {
+                long actionId = action.getId();
+                if (actionId == ACTION_VIEW_SCHEDULE) {
+                    DvrUiHelper.startSchedulesActivity(getContext(), mScheduledRecording);
+                } else if (actionId == ACTION_CANCEL) {
+                    mDvrManager.removeScheduledRecording(mScheduledRecording);
+                } else if (actionId == ACTION_SCHEDULE_RECORDING) {
+                    DvrUiHelper.checkStorageStatusAndShowErrorMessage(
+                            getActivity(),
+                            mInputId,
+                            () ->
+                                    DvrUiHelper.requestRecordingFutureProgram(
+                                            getActivity(), mProgram, false));
+                }
+            }
+        };
+    }
+
+    /** Loads logo and background images for detail fragments. */
+    protected void onLoadLogoAndBackgroundImages(DetailsContent detailsContent) {
+        Drawable logoDrawable = null;
+        Drawable backgroundDrawable = null;
+        if (TextUtils.isEmpty(detailsContent.getLogoImageUri())) {
+            logoDrawable =
+                    getContext().getResources().getDrawable(R.drawable.dvr_default_poster, null);
+            mDetailsOverview.setImageDrawable(logoDrawable);
+        }
+        if (TextUtils.isEmpty(detailsContent.getBackgroundImageUri())) {
+            backgroundDrawable =
+                    getContext().getResources().getDrawable(R.drawable.dvr_default_poster, null);
+            mBackgroundHelper.setBackground(backgroundDrawable);
+        }
+        if (logoDrawable != null && backgroundDrawable != null) {
+            return;
+        }
+        if (logoDrawable == null
+                && backgroundDrawable == null
+                && detailsContent
+                        .getLogoImageUri()
+                        .equals(detailsContent.getBackgroundImageUri())) {
+            ImageLoader.loadBitmap(
+                    getContext(),
+                    detailsContent.getLogoImageUri(),
+                    new MyImageLoaderCallback(
+                            this, LOAD_LOGO_IMAGE | LOAD_BACKGROUND_IMAGE, getContext()));
+            return;
+        }
+        if (logoDrawable == null) {
+            int imageWidth = getResources().getDimensionPixelSize(R.dimen.dvr_details_poster_width);
+            int imageHeight =
+                    getResources().getDimensionPixelSize(R.dimen.dvr_details_poster_height);
+            ImageLoader.loadBitmap(
+                    getContext(),
+                    detailsContent.getLogoImageUri(),
+                    imageWidth,
+                    imageHeight,
+                    new MyImageLoaderCallback(this, LOAD_LOGO_IMAGE, getContext()));
+        }
+        if (backgroundDrawable == null) {
+            ImageLoader.loadBitmap(
+                    getContext(),
+                    detailsContent.getBackgroundImageUri(),
+                    new MyImageLoaderCallback(this, LOAD_BACKGROUND_IMAGE, getContext()));
+        }
+    }
+
+    @Override
+    public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) {
+        for (ScheduledRecording recording : scheduledRecordings) {
+            if (recording.getProgramId() == mProgram.getId()) {
+                mScheduledRecording = recording;
+                updateActions();
+                return;
+            }
+        }
+    }
+
+    @Override
+    public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
+        if (mScheduledRecording == null) {
+            return;
+        }
+        for (ScheduledRecording recording : scheduledRecordings) {
+            if (recording.getId() == mScheduledRecording.getId()) {
+                mScheduledRecording = null;
+                updateActions();
+                return;
+            }
+        }
+    }
+
+    @Override
+    public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) {
+        if (mScheduledRecording == null) {
+            return;
+        }
+        for (ScheduledRecording recording : scheduledRecordings) {
+            if (recording.getId() == mScheduledRecording.getId()) {
+                mScheduledRecording = recording;
+                updateActions();
+                return;
+            }
+        }
+    }
+
+    @Override
+    public void onConflictStateChange(boolean conflict, ScheduledRecording... scheduledRecordings) {
+        onScheduledRecordingStatusChanged(scheduledRecordings);
+    }
+
+    private static class MyImageLoaderCallback
+            extends ImageLoader.ImageLoaderCallback<ProgramDetailsFragment> {
+        private final Context mContext;
+        private final int mLoadType;
+
+        public MyImageLoaderCallback(
+                ProgramDetailsFragment fragment, int loadType, Context context) {
+            super(fragment);
+            mLoadType = loadType;
+            mContext = context;
+        }
+
+        @Override
+        public void onBitmapLoaded(ProgramDetailsFragment fragment, @Nullable Bitmap bitmap) {
+            Drawable drawable;
+            int loadType = mLoadType;
+            if (bitmap == null) {
+                Resources res = mContext.getResources();
+                drawable = res.getDrawable(R.drawable.dvr_default_poster, null);
+                if ((loadType & LOAD_BACKGROUND_IMAGE) != 0 && !fragment.isDetached()) {
+                    loadType &= ~LOAD_BACKGROUND_IMAGE;
+                    fragment.mBackgroundHelper.setBackgroundColor(
+                            res.getColor(R.color.dvr_detail_default_background));
+                    fragment.mBackgroundHelper.setScrim(
+                            res.getColor(R.color.dvr_detail_default_background_scrim));
+                }
+            } else {
+                drawable = new BitmapDrawable(mContext.getResources(), bitmap);
+            }
+            if (!fragment.isDetached()) {
+                if ((loadType & LOAD_LOGO_IMAGE) != 0) {
+                    fragment.mDetailsOverview.setImageDrawable(drawable);
+                }
+                if ((loadType & LOAD_BACKGROUND_IMAGE) != 0) {
+                    fragment.mBackgroundHelper.setBackground(drawable);
+                }
+            }
+        }
+    }
+}
diff --git a/src/com/android/tv/ui/TunableTvView.java b/src/com/android/tv/ui/TunableTvView.java
index bb98d97..5ac6bd8 100644
--- a/src/com/android/tv/ui/TunableTvView.java
+++ b/src/com/android/tv/ui/TunableTvView.java
@@ -20,11 +20,7 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.TimeInterpolator;
 import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.ApplicationErrorReport;
 import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
@@ -38,7 +34,6 @@
 import android.media.tv.TvTrackInfo;
 import android.media.tv.TvView;
 import android.media.tv.TvView.OnUnhandledInputEventListener;
-import android.media.tv.TvView.TvInputCallback;
 import android.net.ConnectivityManager;
 import android.net.Uri;
 import android.os.AsyncTask;
@@ -47,6 +42,7 @@
 import android.support.annotation.IntDef;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
 import android.text.TextUtils;
 import android.text.format.DateUtils;
 import android.util.AttributeSet;
@@ -55,16 +51,17 @@
 import android.view.MotionEvent;
 import android.view.SurfaceView;
 import android.view.View;
+import android.view.accessibility.AccessibilityManager;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 import com.android.tv.InputSessionManager;
 import com.android.tv.InputSessionManager.TvViewSession;
 import com.android.tv.R;
-import com.android.tv.TvFeatures;
 import com.android.tv.TvSingletons;
 import com.android.tv.analytics.Tracker;
-import com.android.tv.common.BuildConfig;
 import com.android.tv.common.CommonConstants;
+import com.android.tv.common.compat.TvInputConstantCompat;
+import com.android.tv.common.compat.TvViewCompat.TvInputCallbackCompat;
 import com.android.tv.common.feature.CommonFeatures;
 import com.android.tv.common.util.CommonUtils;
 import com.android.tv.common.util.Debug;
@@ -75,9 +72,11 @@
 import com.android.tv.data.StreamInfo;
 import com.android.tv.data.WatchedHistoryManager;
 import com.android.tv.data.api.Channel;
+import com.android.tv.features.TvFeatures;
 import com.android.tv.parental.ContentRatingsManager;
 import com.android.tv.parental.ParentalControlSettings;
 import com.android.tv.recommendation.NotificationService;
+import com.android.tv.ui.api.TunableTvViewPlayingApi;
 import com.android.tv.util.NetworkUtils;
 import com.android.tv.util.TvInputManagerHelper;
 import com.android.tv.util.Utils;
@@ -95,8 +94,7 @@
     public static final int VIDEO_UNAVAILABLE_REASON_NO_RESOURCE = -2;
     public static final int VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED = -3;
     public static final int VIDEO_UNAVAILABLE_REASON_NONE = -100;
-
-    private OnTalkBackDpadKeyListener mOnTalkBackDpadKeyListener;
+    private final AccessibilityManager mAccessibilityManager;
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({BLOCK_SCREEN_TYPE_NO_UI, BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW, BLOCK_SCREEN_TYPE_NORMAL})
@@ -132,7 +130,7 @@
 
     private AppLayerTvView mTvView;
     private TvViewSession mTvViewSession;
-    private Channel mCurrentChannel;
+    @Nullable private Channel mCurrentChannel;
     private TvInputManagerHelper mInputManagerHelper;
     private ContentRatingsManager mContentRatingsManager;
     private ParentalControlSettings mParentalControlSettings;
@@ -190,8 +188,10 @@
     private final ConnectivityManager mConnectivityManager;
     private final InputSessionManager mInputSessionManager;
 
-    private final TvInputCallback mCallback =
-            new TvInputCallback() {
+    private int mChannelSignalStrength;
+
+    private final TvInputCallbackCompat mCallback =
+            new TvInputCallbackCompat() {
                 @Override
                 public void onConnectionFailed(String inputId) {
                     Log.w(TAG, "Failed to bind an input");
@@ -252,7 +252,7 @@
                         }
                     }
                     if (mOnTuneListener != null) {
-                        mOnTuneListener.onStreamInfoChanged(TunableTvView.this);
+                        mOnTuneListener.onStreamInfoChanged(TunableTvView.this, true);
                     }
                 }
 
@@ -305,7 +305,10 @@
                         }
                     }
                     if (mOnTuneListener != null) {
-                        mOnTuneListener.onStreamInfoChanged(TunableTvView.this);
+                        // should not change audio track automatically when an audio track or a
+                        // subtitle track is selected
+                        mOnTuneListener.onStreamInfoChanged(
+                                TunableTvView.this, type == TvTrackInfo.TYPE_VIDEO);
                     }
                 }
 
@@ -316,60 +319,15 @@
                             .log(
                                     "Start up of Live TV ends,"
                                             + " TunableTvView.onVideoAvailable resets timer");
-                    long startUpDurationTime = Debug.getTimer(Debug.TAG_START_UP_TIMER).reset();
+                    Debug.getTimer(Debug.TAG_START_UP_TIMER).reset();
                     Debug.removeTimer(Debug.TAG_START_UP_TIMER);
-                    if (BuildConfig.ENG
-                            && startUpDurationTime > Debug.TIME_START_UP_DURATION_THRESHOLD) {
-                        showAlertDialogForLongStartUp();
-                    }
                     mVideoUnavailableReason = VIDEO_UNAVAILABLE_REASON_NONE;
                     updateBlockScreenAndMuting();
                     if (mOnTuneListener != null) {
-                        mOnTuneListener.onStreamInfoChanged(TunableTvView.this);
+                        mOnTuneListener.onStreamInfoChanged(TunableTvView.this, true);
                     }
                 }
 
-                private void showAlertDialogForLongStartUp() {
-                    new AlertDialog.Builder(getContext())
-                            .setTitle(getContext().getString(R.string.settings_send_feedback))
-                            .setMessage(
-                                    "Because the start up time of Live channels is too long,"
-                                            + " please send feedback")
-                            .setPositiveButton(
-                                    android.R.string.ok,
-                                    new DialogInterface.OnClickListener() {
-                                        @Override
-                                        public void onClick(
-                                                DialogInterface dialogInterface, int i) {
-                                            Intent intent = new Intent(Intent.ACTION_APP_ERROR);
-                                            ApplicationErrorReport report =
-                                                    new ApplicationErrorReport();
-                                            report.packageName =
-                                                    report.processName =
-                                                            getContext()
-                                                                    .getApplicationContext()
-                                                                    .getPackageName();
-                                            report.time = System.currentTimeMillis();
-                                            report.type = ApplicationErrorReport.TYPE_CRASH;
-
-                                            // Add the crash info to add title of feedback
-                                            // automatically.
-                                            ApplicationErrorReport.CrashInfo crash =
-                                                    new ApplicationErrorReport.CrashInfo();
-                                            crash.exceptionClassName =
-                                                    "Live TV start up takes long time";
-                                            crash.exceptionMessage =
-                                                    "The start up time of Live TV is too long";
-                                            report.crashInfo = crash;
-
-                                            intent.putExtra(Intent.EXTRA_BUG_REPORT, report);
-                                            getContext().startActivity(intent);
-                                        }
-                                    })
-                            .setNegativeButton(android.R.string.cancel, null)
-                            .show();
-                }
-
                 @Override
                 public void onVideoUnavailable(String inputId, int reason) {
                     if (reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING
@@ -390,12 +348,13 @@
                     }
                     updateBlockScreenAndMuting();
                     if (mOnTuneListener != null) {
-                        mOnTuneListener.onStreamInfoChanged(TunableTvView.this);
+                        mOnTuneListener.onStreamInfoChanged(TunableTvView.this, true);
                     }
                     switch (reason) {
                         case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
                         case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING:
                         case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
+                        case CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED:
                             mTracker.sendChannelVideoUnavailable(mCurrentChannel, reason);
                             break;
                         default:
@@ -441,6 +400,14 @@
                     boolean available = status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE;
                     setTimeShiftAvailable(available);
                 }
+
+                @Override
+                public void onSignalStrength(String inputId, int value) {
+                    mChannelSignalStrength = value;
+                    if (mOnTuneListener != null) {
+                        mOnTuneListener.onChannelSignalStrength();
+                    }
+                }
             };
 
     public TunableTvView(Context context) {
@@ -502,35 +469,12 @@
                                 }
                             }
                         });
-        View placeholder = findViewById(R.id.placeholder);
-        placeholder.requestFocus();
-        findViewById(R.id.channel_up)
-                .setOnFocusChangeListener(
-                        (v, hasFocus) -> {
-                            if (hasFocus) {
-                                placeholder.requestFocus();
-                                if (mOnTalkBackDpadKeyListener != null) {
-                                    mOnTalkBackDpadKeyListener.onTalkBackDpadKey(
-                                            KeyEvent.KEYCODE_DPAD_UP);
-                                }
-                            }
-                        });
-        findViewById(R.id.channel_down)
-                .setOnFocusChangeListener(
-                        (v, hasFocus) -> {
-                            if (hasFocus) {
-                                placeholder.requestFocus();
-                                if (mOnTalkBackDpadKeyListener != null) {
-                                    mOnTalkBackDpadKeyListener.onTalkBackDpadKey(
-                                            KeyEvent.KEYCODE_DPAD_DOWN);
-                                }
-                            }
-                        });
+        mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
     }
 
     public void initialize(
             ProgramDataManager programDataManager, TvInputManagerHelper tvInputManagerHelper) {
-        mTvView = (AppLayerTvView) findViewById(R.id.tv_view);
+        mTvView = findViewById(R.id.tv_view);
         mProgramDataManager = programDataManager;
         mInputManagerHelper = tvInputManagerHelper;
         mContentRatingsManager = tvInputManagerHelper.getContentRatingsManager();
@@ -621,6 +565,14 @@
         mIsUnderShrunken = isUnderShrunken;
     }
 
+    public int getChannelSignalStrength() {
+        return mChannelSignalStrength;
+    }
+
+    public void resetChannelSignalStrength() {
+        mChannelSignalStrength = TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED;
+    }
+
     @Override
     public boolean isPlaying() {
         return mStarted;
@@ -714,12 +666,13 @@
         }
         updateBlockScreenAndMuting();
         if (mOnTuneListener != null) {
-            mOnTuneListener.onStreamInfoChanged(this);
+            mOnTuneListener.onStreamInfoChanged(this, true);
         }
         return true;
     }
 
     @Override
+    @Nullable
     public Channel getCurrentChannel() {
         return mCurrentChannel;
     }
@@ -795,13 +748,15 @@
 
         void onUnexpectedStop(Channel channel);
 
-        void onStreamInfoChanged(StreamInfo info);
+        void onStreamInfoChanged(StreamInfo info, boolean allowAutoSelectionOfTrack);
 
         void onChannelRetuned(Uri channel);
 
         void onContentBlocked();
 
         void onContentAllowed();
+
+        void onChannelSignalStrength();
     }
 
     public void unblockContent(TvContentRating rating) {
@@ -869,14 +824,15 @@
         mTvView.setOnUnhandledInputEventListener(listener);
     }
 
-    public void setOnTalkBackDpadKeyListener(OnTalkBackDpadKeyListener listener) {
-        mOnTalkBackDpadKeyListener = listener;
-    }
-
     public void setClosedCaptionEnabled(boolean enabled) {
         mTvView.setCaptionEnabled(enabled);
     }
 
+    @VisibleForTesting
+    public void setOnTuneListener(OnTuneListener listener) {
+        mOnTuneListener = listener;
+    }
+
     public List<TvTrackInfo> getTracks(int type) {
         return mTvView.getTracks(type);
     }
@@ -1044,6 +1000,7 @@
         if (text != null) {
             mBlockScreenView.setInfoText(text);
         }
+        mBlockScreenView.setInfoTextClickable(mScreenBlocked && mParentControlEnabled);
     }
 
     /**
@@ -1053,6 +1010,8 @@
     private String getBlockScreenText() {
         // TODO: add a test for this method
         Resources res = getResources();
+        boolean isA11y = mAccessibilityManager.isEnabled();
+
         if (mScreenBlocked && mParentControlEnabled) {
             switch (mBlockScreenType) {
                 case BLOCK_SCREEN_TYPE_NO_UI:
@@ -1060,7 +1019,10 @@
                     return "";
                 case BLOCK_SCREEN_TYPE_NORMAL:
                     if (mCanModifyParentalControls) {
-                        return res.getString(R.string.tvview_channel_locked);
+                        return res.getString(
+                                isA11y
+                                        ? R.string.tvview_channel_locked_talkback
+                                        : R.string.tvview_channel_locked);
                     } else {
                         return res.getString(R.string.tvview_channel_locked_no_permission);
                     }
@@ -1081,15 +1043,26 @@
                 case BLOCK_SCREEN_TYPE_NORMAL:
                     if (TextUtils.isEmpty(name)) {
                         if (mCanModifyParentalControls) {
-                            return res.getString(R.string.tvview_content_locked);
+                            return res.getString(
+                                    isA11y
+                                            ? R.string.tvview_content_locked_talkback
+                                            : R.string.tvview_content_locked);
                         } else {
                             return res.getString(R.string.tvview_content_locked_no_permission);
                         }
                     } else {
                         if (mCanModifyParentalControls) {
                             return name.equals(res.getString(R.string.unrated_rating_name))
-                                    ? res.getString(R.string.tvview_content_locked_unrated)
-                                    : res.getString(R.string.tvview_content_locked_format, name);
+                                    ? res.getString(
+                                            isA11y
+                                                    ? R.string
+                                                            .tvview_content_locked_unrated_talkback
+                                                    : R.string.tvview_content_locked_unrated)
+                                    : res.getString(
+                                            isA11y
+                                                    ? R.string.tvview_content_locked_format_talkback
+                                                    : R.string.tvview_content_locked_format,
+                                            name);
                         } else {
                             return name.equals(res.getString(R.string.unrated_rating_name))
                                     ? res.getString(
@@ -1106,6 +1079,8 @@
                     return res.getString(R.string.tvview_msg_audio_only);
                 case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
                     return res.getString(R.string.tvview_msg_weak_signal);
+                case CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED:
+                    return res.getString(R.string.msg_channel_unavailable_not_connected);
                 case VIDEO_UNAVAILABLE_REASON_NO_RESOURCE:
                     return getTuneConflictMessage();
                 default:
@@ -1122,7 +1097,9 @@
                 && (mScreenBlocked
                         || mBlockedContentRating != null
                         || mVideoUnavailableReason
-                                == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN)) {
+                                == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN
+                        || mVideoUnavailableReason
+                                == CommonConstants.VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED)) {
             ((Activity) getContext()).finish();
             return true;
         }
@@ -1237,20 +1214,11 @@
                 .setDuration(durationMillis)
                 .setInterpolator(interpolator)
                 .withStartAction(
-                        new Runnable() {
-                            @Override
-                            public void run() {
-                                mFadeState = FADING_OUT;
-                                mActionAfterFade = actionAfterFade;
-                            }
+                        () -> {
+                            mFadeState = FADING_OUT;
+                            mActionAfterFade = actionAfterFade;
                         })
-                .withEndAction(
-                        new Runnable() {
-                            @Override
-                            public void run() {
-                                mFadeState = FADED_OUT;
-                            }
-                        });
+                .withEndAction(() -> mFadeState = FADED_OUT);
     }
 
     /** Fade in this TunableTvView. Fade in by decreasing the dimming. */
@@ -1264,20 +1232,14 @@
                 .setDuration(durationMillis)
                 .setInterpolator(interpolator)
                 .withStartAction(
-                        new Runnable() {
-                            @Override
-                            public void run() {
-                                mFadeState = FADING_IN;
-                                mActionAfterFade = actionAfterFade;
-                            }
+                        () -> {
+                            mFadeState = FADING_IN;
+                            mActionAfterFade = actionAfterFade;
                         })
                 .withEndAction(
-                        new Runnable() {
-                            @Override
-                            public void run() {
-                                mFadeState = FADED_IN;
-                                mDimScreenView.setVisibility(View.GONE);
-                            }
+                        () -> {
+                            mFadeState = FADED_IN;
+                            mDimScreenView.setVisibility(View.GONE);
                         });
     }
 
@@ -1298,6 +1260,10 @@
         mTimeShiftListener = listener;
     }
 
+    public void setBlockedInfoOnClickListener(@Nullable OnClickListener onClickListener) {
+        mBlockScreenView.setInfoTextOnClickListener(onClickListener);
+    }
+
     private void setTimeShiftAvailable(boolean isTimeShiftAvailable) {
         if (mTimeShiftAvailable == isTimeShiftAvailable) {
             return;
@@ -1336,7 +1302,7 @@
 
     /** Plays the media, if the current input supports time-shifting. */
     @Override
-    public void timeshiftPlay() {
+    public void timeShiftPlay() {
         if (!isTimeShiftAvailable()) {
             throw new IllegalStateException("Time-shift is not supported for the current channel");
         }
@@ -1348,7 +1314,7 @@
 
     /** Pauses the media, if the current input supports time-shifting. */
     @Override
-    public void timeshiftPause() {
+    public void timeShiftPause() {
         if (!isTimeShiftAvailable()) {
             throw new IllegalStateException("Time-shift is not supported for the current channel");
         }
@@ -1364,7 +1330,7 @@
      * @param speed The speed to rewind the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x.
      */
     @Override
-    public void timeshiftRewind(int speed) {
+    public void timeShiftRewind(int speed) {
         if (!isTimeShiftAvailable()) {
             throw new IllegalStateException("Time-shift is not supported for the current channel");
         } else {
@@ -1384,7 +1350,7 @@
      * @param speed The speed to forward the media. e.g. 2 for 2x, 3 for 3x and 4 for 4x.
      */
     @Override
-    public void timeshiftFastForward(int speed) {
+    public void timeShiftFastForward(int speed) {
         if (!isTimeShiftAvailable()) {
             throw new IllegalStateException("Time-shift is not supported for the current channel");
         } else {
@@ -1404,7 +1370,7 @@
      * @param timeMs The time in milliseconds to seek to.
      */
     @Override
-    public void timeshiftSeekTo(long timeMs) {
+    public void timeShiftSeekTo(long timeMs) {
         if (!isTimeShiftAvailable()) {
             throw new IllegalStateException("Time-shift is not supported for the current channel");
         }
@@ -1413,14 +1379,14 @@
 
     /** Returns the current playback position in milliseconds. */
     @Override
-    public long timeshiftGetCurrentPositionMs() {
+    public long timeShiftGetCurrentPositionMs() {
         if (!isTimeShiftAvailable()) {
             throw new IllegalStateException("Time-shift is not supported for the current channel");
         }
         if (DEBUG) {
             Log.d(
                     TAG,
-                    "timeshiftGetCurrentPositionMs: current position ="
+                    "timeShiftGetCurrentPositionMs: current position ="
                             + Utils.toTimeString(mTimeShiftCurrentPositionMs));
         }
         return mTimeShiftCurrentPositionMs;
@@ -1446,12 +1412,6 @@
         };
     }
 
-    /** Listens for dpad actions that are otherwise trapped by talkback */
-    public interface OnTalkBackDpadKeyListener {
-
-        void onTalkBackDpadKey(int keycode);
-    }
-
     /** A listener which receives the notification when the screen is blocked/unblocked. */
     public abstract static class OnScreenBlockingChangedListener {
         /** Called when the screen is blocked/unblocked. */
diff --git a/src/com/android/tv/ui/TvOverlayManager.java b/src/com/android/tv/ui/TvOverlayManager.java
index 222fcb3..b2854a1 100644
--- a/src/com/android/tv/ui/TvOverlayManager.java
+++ b/src/com/android/tv/ui/TvOverlayManager.java
@@ -86,19 +86,18 @@
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(
-        flag = true,
-        value = {
-            FLAG_HIDE_OVERLAYS_DEFAULT,
-            FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION,
-            FLAG_HIDE_OVERLAYS_KEEP_SCENE,
-            FLAG_HIDE_OVERLAYS_KEEP_DIALOG,
-            FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS,
-            FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY,
-            FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE,
-            FLAG_HIDE_OVERLAYS_KEEP_MENU,
-            FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT
-        }
-    )
+            flag = true,
+            value = {
+                FLAG_HIDE_OVERLAYS_DEFAULT,
+                FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION,
+                FLAG_HIDE_OVERLAYS_KEEP_SCENE,
+                FLAG_HIDE_OVERLAYS_KEEP_DIALOG,
+                FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS,
+                FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY,
+                FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE,
+                FLAG_HIDE_OVERLAYS_KEEP_MENU,
+                FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT
+            })
     private @interface HideOverlayFlag {}
     // FLAG_HIDE_OVERLAYs must be bitwise exclusive.
     public static final int FLAG_HIDE_OVERLAYS_DEFAULT = 0b000000000;
@@ -115,20 +114,19 @@
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(
-        flag = true,
-        value = {
-            OVERLAY_TYPE_NONE,
-            OVERLAY_TYPE_MENU,
-            OVERLAY_TYPE_SIDE_FRAGMENT,
-            OVERLAY_TYPE_DIALOG,
-            OVERLAY_TYPE_GUIDE,
-            OVERLAY_TYPE_SCENE_CHANNEL_BANNER,
-            OVERLAY_TYPE_SCENE_INPUT_BANNER,
-            OVERLAY_TYPE_SCENE_KEYPAD_CHANNEL_SWITCH,
-            OVERLAY_TYPE_SCENE_SELECT_INPUT,
-            OVERLAY_TYPE_FRAGMENT
-        }
-    )
+            flag = true,
+            value = {
+                OVERLAY_TYPE_NONE,
+                OVERLAY_TYPE_MENU,
+                OVERLAY_TYPE_SIDE_FRAGMENT,
+                OVERLAY_TYPE_DIALOG,
+                OVERLAY_TYPE_GUIDE,
+                OVERLAY_TYPE_SCENE_CHANNEL_BANNER,
+                OVERLAY_TYPE_SCENE_INPUT_BANNER,
+                OVERLAY_TYPE_SCENE_KEYPAD_CHANNEL_SWITCH,
+                OVERLAY_TYPE_SCENE_SELECT_INPUT,
+                OVERLAY_TYPE_FRAGMENT
+            })
     private @interface TvOverlayType {}
     // OVERLAY_TYPEs must be bitwise exclusive.
     /** The overlay type which indicates that there are no overlays. */
@@ -176,6 +174,8 @@
     public static final int UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK = 5;
     /** Updates channel banner because of stream info updating. */
     public static final int UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO = 6;
+    /** Updates channel banner because of channel signal updating. */
+    public static final int UPDATE_CHANNEL_BANNER_REASON_UPDATE_SIGNAL_STRENGTH = 7;
 
     private static final String FRAGMENT_TAG_SETUP_SOURCES = "tag_setup_sources";
     private static final String FRAGMENT_TAG_NEW_SOURCES = "tag_new_sources";
@@ -287,35 +287,17 @@
         mSideFragmentManager =
                 new SideFragmentManager(
                         mainActivity,
-                        new Runnable() {
-                            @Override
-                            public void run() {
-                                onOverlayOpened(OVERLAY_TYPE_SIDE_FRAGMENT);
-                                hideOverlays(FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS);
-                            }
+                        () -> {
+                            onOverlayOpened(OVERLAY_TYPE_SIDE_FRAGMENT);
+                            hideOverlays(FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS);
                         },
-                        new Runnable() {
-                            @Override
-                            public void run() {
-                                showChannelBannerIfHiddenBySideFragment();
-                                onOverlayClosed(OVERLAY_TYPE_SIDE_FRAGMENT);
-                            }
+                        () -> {
+                            showChannelBannerIfHiddenBySideFragment();
+                            onOverlayClosed(OVERLAY_TYPE_SIDE_FRAGMENT);
                         });
         // Program Guide
-        Runnable preShowRunnable =
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        onOverlayOpened(OVERLAY_TYPE_GUIDE);
-                    }
-                };
-        Runnable postHideRunnable =
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        onOverlayClosed(OVERLAY_TYPE_GUIDE);
-                    }
-                };
+        Runnable preShowRunnable = () -> onOverlayOpened(OVERLAY_TYPE_GUIDE);
+        Runnable postHideRunnable = () -> onOverlayClosed(OVERLAY_TYPE_GUIDE);
         DvrDataManager dvrDataManager =
                 CommonFeatures.DVR.isEnabled(mainActivity) ? singletons.getDvrDataManager() : null;
         mProgramGuide =
@@ -520,16 +502,13 @@
         hideOverlays(FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
         onOverlayOpened(OVERLAY_TYPE_FRAGMENT);
         runAfterSideFragmentsAreClosed(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        if (DEBUG) Log.d(TAG, "showFragment(" + fragment + ")");
-                        mMainActivity
-                                .getFragmentManager()
-                                .beginTransaction()
-                                .replace(R.id.fragment_container, fragment, tag)
-                                .commit();
-                    }
+                () -> {
+                    if (DEBUG) Log.d(TAG, "showFragment(" + fragment + ")");
+                    mMainActivity
+                            .getFragmentManager()
+                            .beginTransaction()
+                            .replace(R.id.fragment_container, fragment, tag)
+                            .commit();
                 });
     }
 
@@ -678,12 +657,7 @@
     /** Shows the program guide. */
     public void showProgramGuide() {
         mProgramGuide.show(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE);
-                    }
-                });
+                () -> hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE));
     }
 
     /**
@@ -855,6 +829,10 @@
                         && lockType != ChannelBannerView.LOCK_PROGRAM_DETAIL) {
                     mChannelBannerView.updateViews(false);
                 }
+            } else if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mMainActivity)
+                    && reason == UPDATE_CHANNEL_BANNER_REASON_UPDATE_SIGNAL_STRENGTH) {
+                mChannelBannerView.updateChannelSignalStrengthView(
+                        mTvView.getChannelSignalStrength());
             } else {
                 mChannelBannerView.updateViews(
                         reason == UPDATE_CHANNEL_BANNER_REASON_TUNE
diff --git a/src/com/android/tv/ui/TvTransitionManager.java b/src/com/android/tv/ui/TvTransitionManager.java
index 5af3e6f..f60337f 100644
--- a/src/com/android/tv/ui/TvTransitionManager.java
+++ b/src/com/android/tv/ui/TvTransitionManager.java
@@ -174,28 +174,19 @@
 
         mEmptyScene = new Scene(mSceneContainer, (View) mEmptyView);
         mEmptyScene.setEnterAction(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        FrameLayout.LayoutParams emptySceneLayoutParams =
-                                (FrameLayout.LayoutParams) mEmptyView.getLayoutParams();
-                        ViewGroup.MarginLayoutParams lp =
-                                (ViewGroup.MarginLayoutParams) mCurrentSceneView.getLayoutParams();
-                        emptySceneLayoutParams.topMargin = mCurrentSceneView.getTop();
-                        emptySceneLayoutParams.setMarginStart(lp.getMarginStart());
-                        emptySceneLayoutParams.height = mCurrentSceneView.getHeight();
-                        emptySceneLayoutParams.width = mCurrentSceneView.getWidth();
-                        mEmptyView.setLayoutParams(emptySceneLayoutParams);
-                        setCurrentScene(mEmptyScene, mEmptyView);
-                    }
+                () -> {
+                    FrameLayout.LayoutParams emptySceneLayoutParams =
+                            (FrameLayout.LayoutParams) mEmptyView.getLayoutParams();
+                    ViewGroup.MarginLayoutParams lp =
+                            (ViewGroup.MarginLayoutParams) mCurrentSceneView.getLayoutParams();
+                    emptySceneLayoutParams.topMargin = mCurrentSceneView.getTop();
+                    emptySceneLayoutParams.setMarginStart(lp.getMarginStart());
+                    emptySceneLayoutParams.height = mCurrentSceneView.getHeight();
+                    emptySceneLayoutParams.width = mCurrentSceneView.getWidth();
+                    mEmptyView.setLayoutParams(emptySceneLayoutParams);
+                    setCurrentScene(mEmptyScene, mEmptyView);
                 });
-        mEmptyScene.setExitAction(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        removeAllViewsFromOverlay();
-                    }
-                });
+        mEmptyScene.setExitAction(this::removeAllViewsFromOverlay);
 
         mChannelBannerScene = buildScene(mSceneContainer, mChannelBannerView);
         mInputBannerScene = buildScene(mSceneContainer, mInputBannerView);
@@ -274,21 +265,15 @@
     private Scene buildScene(ViewGroup sceneRoot, final TransitionLayout layout) {
         final Scene scene = new Scene(sceneRoot, (View) layout);
         scene.setEnterAction(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        boolean wasEmptyScene = (mCurrentScene == mEmptyScene);
-                        setCurrentScene(scene, (ViewGroup) layout);
-                        layout.onEnterAction(wasEmptyScene);
-                    }
+                () -> {
+                    boolean wasEmptyScene = (mCurrentScene == mEmptyScene);
+                    setCurrentScene(scene, (ViewGroup) layout);
+                    layout.onEnterAction(wasEmptyScene);
                 });
         scene.setExitAction(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        removeAllViewsFromOverlay();
-                        layout.onExitAction();
-                    }
+                () -> {
+                    removeAllViewsFromOverlay();
+                    layout.onExitAction();
                 });
         return scene;
     }
diff --git a/src/com/android/tv/ui/TvViewUiManager.java b/src/com/android/tv/ui/TvViewUiManager.java
index 7e354db..b7e8b43 100644
--- a/src/com/android/tv/ui/TvViewUiManager.java
+++ b/src/com/android/tv/ui/TvViewUiManager.java
@@ -43,9 +43,9 @@
 import android.view.animation.AnimationUtils;
 import android.widget.FrameLayout;
 import com.android.tv.R;
-import com.android.tv.TvFeatures;
 import com.android.tv.TvOptionsManager;
 import com.android.tv.data.DisplayMode;
+import com.android.tv.features.TvFeatures;
 import com.android.tv.util.TvSettings;
 
 /**
@@ -460,12 +460,7 @@
                             return;
                         }
                         mHandler.post(
-                                new Runnable() {
-                                    @Override
-                                    public void run() {
-                                        setTvViewPosition(mTvViewLayoutParams, mTvViewFrame, false);
-                                    }
-                                });
+                                () -> setTvViewPosition(mTvViewLayoutParams, mTvViewFrame, false));
                     }
                 });
         mTvViewAnimator.addUpdateListener(
@@ -496,13 +491,7 @@
                 new AnimatorListenerAdapter() {
                     @Override
                     public void onAnimationEnd(Animator animation) {
-                        mHandler.post(
-                                new Runnable() {
-                                    @Override
-                                    public void run() {
-                                        mContentView.setBackgroundColor(mBackgroundColor);
-                                    }
-                                });
+                        mHandler.post(() -> mContentView.setBackgroundColor(mBackgroundColor));
                     }
                 });
     }
diff --git a/src/com/android/tv/ui/TunableTvViewPlayingApi.java b/src/com/android/tv/ui/api/TunableTvViewPlayingApi.java
similarity index 85%
rename from src/com/android/tv/ui/TunableTvViewPlayingApi.java
rename to src/com/android/tv/ui/api/TunableTvViewPlayingApi.java
index 3f19b61..eb1f030 100644
--- a/src/com/android/tv/ui/TunableTvViewPlayingApi.java
+++ b/src/com/android/tv/ui/api/TunableTvViewPlayingApi.java
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.tv.ui;
+package com.android.tv.ui.api;
 
 /** API to play pause and set the volume of a TunableTvView */
 public interface TunableTvViewPlayingApi {
@@ -27,17 +27,17 @@
 
     boolean isTimeShiftAvailable();
 
-    void timeshiftPlay();
+    void timeShiftPlay();
 
-    void timeshiftPause();
+    void timeShiftPause();
 
-    void timeshiftRewind(int speed);
+    void timeShiftRewind(int speed);
 
-    void timeshiftFastForward(int speed);
+    void timeShiftFastForward(int speed);
 
-    void timeshiftSeekTo(long timeMs);
+    void timeShiftSeekTo(long timeMs);
 
-    long timeshiftGetCurrentPositionMs();
+    long timeShiftGetCurrentPositionMs();
 
     /** Used to receive the time-shift events. */
     abstract class TimeShiftListener {
diff --git a/src/com/android/tv/ui/hideable/AutoHideScheduler.java b/src/com/android/tv/ui/hideable/AutoHideScheduler.java
index 7585979..8bf70de 100644
--- a/src/com/android/tv/ui/hideable/AutoHideScheduler.java
+++ b/src/com/android/tv/ui/hideable/AutoHideScheduler.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package com.android.tv.ui.hideable;
 
 import android.content.Context;
diff --git a/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java b/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java
index 48b8072..62130b6 100644
--- a/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java
+++ b/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java
@@ -37,7 +37,6 @@
 import com.android.tv.util.Utils;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
 
@@ -213,17 +212,14 @@
         ArrayList<Channel> channels = new ArrayList<>(mChannels);
         Collections.sort(
                 channels,
-                new Comparator<Channel>() {
-                    @Override
-                    public int compare(Channel lhs, Channel rhs) {
-                        boolean lhsHd = isHdChannel(lhs);
-                        boolean rhsHd = isHdChannel(rhs);
-                        if (lhsHd == rhsHd) {
-                            return ChannelNumber.compare(
-                                    lhs.getDisplayNumber(), rhs.getDisplayNumber());
-                        } else {
-                            return lhsHd ? -1 : 1;
-                        }
+                (Channel lhs, Channel rhs) -> {
+                    boolean lhsHd = isHdChannel(lhs);
+                    boolean rhsHd = isHdChannel(rhs);
+                    if (lhsHd == rhsHd) {
+                        return ChannelNumber.compare(
+                                lhs.getDisplayNumber(), rhs.getDisplayNumber());
+                    } else {
+                        return lhsHd ? -1 : 1;
                     }
                 });
 
diff --git a/src/com/android/tv/ui/sidepanel/MultiAudioFragment.java b/src/com/android/tv/ui/sidepanel/MultiAudioFragment.java
index 03b71c8..7a65247 100644
--- a/src/com/android/tv/ui/sidepanel/MultiAudioFragment.java
+++ b/src/com/android/tv/ui/sidepanel/MultiAudioFragment.java
@@ -20,7 +20,7 @@
 import android.text.TextUtils;
 import android.view.KeyEvent;
 import com.android.tv.R;
-import com.android.tv.util.Utils;
+import com.android.tv.util.TvTrackInfoUtils;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -51,12 +51,13 @@
 
         List<Item> items = new ArrayList<>();
         if (tracks != null) {
-            boolean needToShowSampleRate = Utils.needToShowSampleRate(getActivity(), tracks);
+            boolean needToShowSampleRate = TvTrackInfoUtils
+                .needToShowSampleRate(getActivity(), tracks);
             int pos = 0;
             for (final TvTrackInfo track : tracks) {
                 RadioButtonItem item =
                         new MultiAudioOptionItem(
-                                Utils.getMultiAudioString(
+                                TvTrackInfoUtils.getMultiAudioString(
                                         getActivity(), track, needToShowSampleRate),
                                 track.getId());
                 if (track.getId().equals(mSelectedTrackId)) {
diff --git a/src/com/android/tv/ui/sidepanel/SettingsFragment.java b/src/com/android/tv/ui/sidepanel/SettingsFragment.java
index 31d00fa..aa71fb7 100644
--- a/src/com/android/tv/ui/sidepanel/SettingsFragment.java
+++ b/src/com/android/tv/ui/sidepanel/SettingsFragment.java
@@ -16,8 +16,6 @@
 
 package com.android.tv.ui.sidepanel;
 
-import static com.android.tv.TvFeatures.TUNER;
-
 import android.app.ApplicationErrorReport;
 import android.content.Intent;
 import android.media.tv.TvInputInfo;
@@ -81,10 +79,9 @@
         customizeChannelListItem.setEnabled(false);
         items.add(customizeChannelListItem);
         final MainActivity activity = getMainActivity();
+        TvSingletons singletons = TvSingletons.getSingletons(getContext());
         boolean hasNewInput =
-                TvSingletons.getSingletons(getContext())
-                        .getSetupUtils()
-                        .hasNewInput(activity.getTvInputManagerHelper());
+                singletons.getSetupUtils().hasNewInput(activity.getTvInputManagerHelper());
         items.add(
                 new ActionItem(
                         getString(R.string.settings_channel_source_item_setup),
@@ -127,11 +124,9 @@
             // It's TBD.
         }
         boolean showTrickplaySetting = false;
-        if (TUNER.isEnabled(getContext())) {
+        if (singletons.getBuiltInTunerManager().isPresent()) {
             for (TvInputInfo inputInfo :
-                    TvSingletons.getSingletons(getContext())
-                            .getTvInputManagerHelper()
-                            .getTvInputInfos(true, true)) {
+                    singletons.getTvInputManagerHelper().getTvInputInfos(true, true)) {
                 if (Utils.isInternalTvInput(getContext(), inputInfo.getId())) {
                     showTrickplaySetting = true;
                     break;
diff --git a/src/com/android/tv/ui/sidepanel/SideFragment.java b/src/com/android/tv/ui/sidepanel/SideFragment.java
index 2902ea7..590f130 100644
--- a/src/com/android/tv/ui/sidepanel/SideFragment.java
+++ b/src/com/android/tv/ui/sidepanel/SideFragment.java
@@ -342,12 +342,9 @@
             }
             if (view.getBackground() instanceof RippleDrawable) {
                 view.postDelayed(
-                        new Runnable() {
-                            @Override
-                            public void run() {
-                                if (mItem != null) {
-                                    mItem.onSelected();
-                                }
+                        () -> {
+                            if (mItem != null) {
+                                mItem.onSelected();
                             }
                         },
                         view.getResources().getInteger(R.integer.side_panel_ripple_anim_duration));
diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java
index 4e3cf7f..b14bf78 100644
--- a/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java
+++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java
@@ -41,7 +41,6 @@
 import com.android.tv.ui.sidepanel.SideFragment;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
 
 public class ChannelsBlockedFragment extends SideFragment {
@@ -132,15 +131,11 @@
         mChannels.addAll(getChannelDataManager().getChannelList());
         Collections.sort(
                 mChannels,
-                new Comparator<Channel>() {
-                    @Override
-                    public int compare(Channel lhs, Channel rhs) {
-                        if (lhs.isBrowsable() != rhs.isBrowsable()) {
-                            return lhs.isBrowsable() ? -1 : 1;
-                        }
-                        return ChannelNumber.compare(
-                                lhs.getDisplayNumber(), rhs.getDisplayNumber());
+                (Channel lhs, Channel rhs) -> {
+                    if (lhs.isBrowsable() != rhs.isBrowsable()) {
+                        return lhs.isBrowsable() ? -1 : 1;
                     }
+                    return ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber());
                 });
 
         final long currentChannelId = getMainActivity().getCurrentChannelId();
diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java
index 128fcd1..d1ae442 100644
--- a/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java
+++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java
@@ -39,6 +39,7 @@
 import com.android.tv.ui.sidepanel.SideFragment;
 import com.android.tv.util.TvSettings;
 import com.android.tv.util.TvSettings.ContentRatingLevel;
+import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -167,7 +168,7 @@
                             super.onUpdate();
                             setChecked(
                                     mParentalControlSettings.isRatingBlocked(
-                                            new TvContentRating[] {TvContentRating.UNRATED}));
+                                            ImmutableList.of(TvContentRating.UNRATED)));
                         }
 
                         @Override
@@ -239,7 +240,7 @@
                 // set checked if UNRATED is blocked, and set unchecked otherwise.
                 mBlockUnratedItem.setChecked(
                         mParentalControlSettings.isRatingBlocked(
-                                new TvContentRating[] {TvContentRating.UNRATED}));
+                                ImmutableList.of(TvContentRating.UNRATED)));
             }
             notifyItemsChanged(mRatingLevelItems.size());
         }
diff --git a/src/com/android/tv/util/AsyncDbTask.java b/src/com/android/tv/util/AsyncDbTask.java
index 60fa301..b352395 100644
--- a/src/com/android/tv/util/AsyncDbTask.java
+++ b/src/com/android/tv/util/AsyncDbTask.java
@@ -17,6 +17,7 @@
 package com.android.tv.util;
 
 import android.content.ContentResolver;
+import android.content.Context;
 import android.database.Cursor;
 import android.media.tv.TvContract;
 import android.media.tv.TvContract.Programs;
@@ -34,9 +35,12 @@
 import com.android.tv.data.Program;
 import com.android.tv.data.api.Channel;
 import com.android.tv.dvr.data.RecordedProgram;
+import com.google.common.base.Predicate;
+import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Executor;
+import javax.inject.Qualifier;
 
 /**
  * {@link AsyncTask} that defaults to executing on its own single threaded Executor Service.
@@ -50,6 +54,10 @@
     private static final String TAG = "AsyncDbTask";
     private static final boolean DEBUG = false;
 
+    /** Annotation for requesting the {@link Executor} for data base access. */
+    @Qualifier
+    public @interface DbExecutor {}
+
     private final Executor mExecutor;
     boolean mCalledExecuteOnDbThread;
 
@@ -67,23 +75,23 @@
      * @param <Result> the type of result returned by {@link #onQuery(Cursor)}
      */
     public abstract static class AsyncQueryTask<Result> extends AsyncDbTask<Void, Void, Result> {
-        private final ContentResolver mContentResolver;
+        private final WeakReference<Context> mContextReference;
         private final Uri mUri;
-        private final String[] mProjection;
         private final String mSelection;
         private final String[] mSelectionArgs;
         private final String mOrderBy;
+        private String[] mProjection;
 
         public AsyncQueryTask(
-                Executor executor,
-                ContentResolver contentResolver,
+                @DbExecutor Executor executor,
+                Context context,
                 Uri uri,
                 String[] projection,
                 String selection,
                 String[] selectionArgs,
                 String orderBy) {
             super(executor);
-            mContentResolver = contentResolver;
+            mContextReference = new WeakReference<>(context);
             mUri = uri;
             mProjection = projection;
             mSelection = selection;
@@ -110,12 +118,35 @@
                 // This is guaranteed to never call onPostExecute because the task is canceled.
                 return null;
             }
+            Context context = mContextReference.get();
+            if (context == null) {
+                return null;
+            }
+            if (Utils.isProgramsUri(mUri)
+                            && TvProviderUtils.checkSeriesIdColumn(context, Programs.CONTENT_URI)) {
+                mProjection =
+                        TvProviderUtils.addExtraColumnsToProjection(
+                                mProjection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID);
+            } else if (Utils.isRecordedProgramsUri(mUri)) {
+                if (TvProviderUtils.checkSeriesIdColumn(
+                        context, TvContract.RecordedPrograms.CONTENT_URI)) {
+                    mProjection =
+                            TvProviderUtils.addExtraColumnsToProjection(
+                                    mProjection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID);
+                }
+                if (TvProviderUtils.checkStateColumn(
+                        context, TvContract.RecordedPrograms.CONTENT_URI)) {
+                    mProjection =
+                            TvProviderUtils.addExtraColumnsToProjection(
+                                    mProjection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_STATE);
+                }
+            }
             if (DEBUG) {
                 Log.v(TAG, "Starting query for " + this);
             }
             try (Cursor c =
-                    mContentResolver.query(
-                            mUri, mProjection, mSelection, mSelectionArgs, mOrderBy)) {
+                    context.getContentResolver()
+                            .query(mUri, mProjection, mSelection, mSelectionArgs, mOrderBy)) {
                 if (c != null && !isCancelled()) {
                     Result result = onQuery(c);
                     if (DEBUG) {
@@ -164,33 +195,25 @@
 
         public AsyncQueryListTask(
                 Executor executor,
-                ContentResolver contentResolver,
+                Context context,
                 Uri uri,
                 String[] projection,
                 String selection,
                 String[] selectionArgs,
                 String orderBy) {
-            this(
-                    executor,
-                    contentResolver,
-                    uri,
-                    projection,
-                    selection,
-                    selectionArgs,
-                    orderBy,
-                    null);
+            this(executor, context, uri, projection, selection, selectionArgs, orderBy, null);
         }
 
         public AsyncQueryListTask(
                 Executor executor,
-                ContentResolver contentResolver,
+                Context context,
                 Uri uri,
                 String[] projection,
                 String selection,
                 String[] selectionArgs,
                 String orderBy,
                 CursorFilter filter) {
-            super(executor, contentResolver, uri, projection, selection, selectionArgs, orderBy);
+            super(executor, context, uri, projection, selection, selectionArgs, orderBy);
             mFilter = filter;
         }
 
@@ -202,7 +225,7 @@
                     // This is guaranteed to never call onPostExecute because the task is canceled.
                     return null;
                 }
-                if (mFilter != null && !mFilter.filter(c)) {
+                if (mFilter != null && !mFilter.apply(c)) {
                     continue;
                 }
                 T t = fromCursor(c);
@@ -237,13 +260,13 @@
 
         public AsyncQueryItemTask(
                 Executor executor,
-                ContentResolver contentResolver,
+                Context context,
                 Uri uri,
                 String[] projection,
                 String selection,
                 String[] selectionArgs,
                 String orderBy) {
-            super(executor, contentResolver, uri, projection, selection, selectionArgs, orderBy);
+            super(executor, context, uri, projection, selection, selectionArgs, orderBy);
         }
 
         @Override
@@ -283,10 +306,10 @@
     /** Gets an {@link List} of {@link Channel}s from {@link TvContract.Channels#CONTENT_URI}. */
     public abstract static class AsyncChannelQueryTask extends AsyncQueryListTask<Channel> {
 
-        public AsyncChannelQueryTask(Executor executor, ContentResolver contentResolver) {
+        public AsyncChannelQueryTask(Executor executor, Context context) {
             super(
                     executor,
-                    contentResolver,
+                    context,
                     TvContract.Channels.CONTENT_URI,
                     ChannelImpl.PROJECTION,
                     null,
@@ -302,20 +325,13 @@
 
     /** Gets an {@link List} of {@link Program}s from {@link TvContract.Programs#CONTENT_URI}. */
     public abstract static class AsyncProgramQueryTask extends AsyncQueryListTask<Program> {
-        public AsyncProgramQueryTask(Executor executor, ContentResolver contentResolver) {
-            super(
-                    executor,
-                    contentResolver,
-                    Programs.CONTENT_URI,
-                    Program.PROJECTION,
-                    null,
-                    null,
-                    null);
+        public AsyncProgramQueryTask(Executor executor, Context context) {
+            super(executor, context, Programs.CONTENT_URI, Program.PROJECTION, null, null, null);
         }
 
         public AsyncProgramQueryTask(
                 Executor executor,
-                ContentResolver contentResolver,
+                Context context,
                 Uri uri,
                 String selection,
                 String[] selectionArgs,
@@ -323,7 +339,7 @@
                 CursorFilter filter) {
             super(
                     executor,
-                    contentResolver,
+                    context,
                     uri,
                     Program.PROJECTION,
                     selection,
@@ -341,9 +357,8 @@
     /** Gets an {@link List} of {@link TvContract.RecordedPrograms}s. */
     public abstract static class AsyncRecordedProgramQueryTask
             extends AsyncQueryListTask<RecordedProgram> {
-        public AsyncRecordedProgramQueryTask(
-                Executor executor, ContentResolver contentResolver, Uri uri) {
-            super(executor, contentResolver, uri, RecordedProgram.PROJECTION, null, null, null);
+        public AsyncRecordedProgramQueryTask(Executor executor, Context context, Uri uri) {
+            super(executor, context, uri, RecordedProgram.PROJECTION, null, null, null);
         }
 
         @Override
@@ -370,13 +385,10 @@
         protected final long mChannelId;
 
         public LoadProgramsForChannelTask(
-                Executor executor,
-                ContentResolver contentResolver,
-                long channelId,
-                @Nullable Range<Long> period) {
+                Executor executor, Context context, long channelId, @Nullable Range<Long> period) {
             super(
                     executor,
-                    contentResolver,
+                    context,
                     period == null
                             ? TvContract.buildProgramsUriForChannel(channelId)
                             : TvContract.buildProgramsUriForChannel(
@@ -401,11 +413,10 @@
     /** Gets a single {@link Program} from {@link TvContract.Programs#CONTENT_URI}. */
     public static class AsyncQueryProgramTask extends AsyncQueryItemTask<Program> {
 
-        public AsyncQueryProgramTask(
-                Executor executor, ContentResolver contentResolver, long programId) {
+        public AsyncQueryProgramTask(Executor executor, Context context, long programId) {
             super(
                     executor,
-                    contentResolver,
+                    context,
                     TvContract.buildProgramUri(programId),
                     Program.PROJECTION,
                     null,
@@ -420,5 +431,5 @@
     }
 
     /** An interface which filters the row. */
-    public interface CursorFilter extends Filter<Cursor> {}
+    public interface CursorFilter extends Predicate<Cursor> {}
 }
diff --git a/src/com/android/tv/util/RecurringRunner.java b/src/com/android/tv/util/RecurringRunner.java
index 764689c..82e8a94 100644
--- a/src/com/android/tv/util/RecurringRunner.java
+++ b/src/com/android/tv/util/RecurringRunner.java
@@ -99,17 +99,14 @@
         long delay = Math.max(next - now, 0);
         boolean posted =
                 mHandler.postDelayed(
-                        new Runnable() {
-                            @Override
-                            public void run() {
-                                try {
-                                    if (DEBUG) Log.i(TAG, "Starting " + mName);
-                                    mRunnable.run();
-                                } catch (Exception e) {
-                                    Log.w(TAG, "Error running " + mName, e);
-                                }
-                                postAt(resetNextRunTime());
+                        () -> {
+                            try {
+                                if (DEBUG) Log.i(TAG, "Starting " + mName);
+                                mRunnable.run();
+                            } catch (Exception e) {
+                                Log.w(TAG, "Error running " + mName, e);
                             }
+                            postAt(resetNextRunTime());
                         },
                         delay);
         if (!posted) {
diff --git a/src/com/android/tv/util/SetupUtils.java b/src/com/android/tv/util/SetupUtils.java
index 0d53632..a9b67fa 100644
--- a/src/com/android/tv/util/SetupUtils.java
+++ b/src/com/android/tv/util/SetupUtils.java
@@ -28,20 +28,25 @@
 import android.preference.PreferenceManager;
 import android.support.annotation.Nullable;
 import android.support.annotation.UiThread;
-import android.support.annotation.VisibleForTesting;
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
 import com.android.tv.TvSingletons;
-import com.android.tv.common.BaseApplication;
 import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.dagger.annotations.ApplicationContext;
+import com.android.tv.common.singletons.HasTvInputId;
 import com.android.tv.data.ChannelDataManager;
 import com.android.tv.data.api.Channel;
+import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
+import com.google.common.base.Optional;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
+import javax.inject.Inject;
+import javax.inject.Singleton;
 
 /** A utility class related to input setup. */
+@Singleton
 public class SetupUtils {
     private static final String TAG = "SetupUtils";
     private static final boolean DEBUG = false;
@@ -61,10 +66,12 @@
     private final Set<String> mSetUpInputs;
     private final Set<String> mRecognizedInputs;
     private boolean mIsFirstTune;
-    private final String mTunerInputId;
+    private final Optional<String> mOptionalTunerInputId;
 
-    @VisibleForTesting
-    protected SetupUtils(Context context) {
+    @Inject
+    public SetupUtils(
+            @ApplicationContext Context context,
+            Optional<BuiltInTunerManager> optionalBuiltInTunerManager) {
         mContext = context;
         mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
         mSetUpInputs = new ArraySet<>();
@@ -77,16 +84,8 @@
         mRecognizedInputs.addAll(
                 mSharedPreferences.getStringSet(PREF_KEY_RECOGNIZED_INPUTS, mKnownInputs));
         mIsFirstTune = mSharedPreferences.getBoolean(PREF_KEY_IS_FIRST_TUNE, true);
-        mTunerInputId = BaseApplication.getSingletons(context).getEmbeddedTunerInputId();
-    }
-
-    /**
-     * Creates an instance of {@link SetupUtils}.
-     *
-     * <p><b>WARNING</b> this should only be called by the top level application.
-     */
-    public static SetupUtils createForTvSingletons(Context context) {
-        return new SetupUtils(context.getApplicationContext());
+        mOptionalTunerInputId =
+                optionalBuiltInTunerManager.transform(HasTvInputId::getEmbeddedTunerInputId);
     }
 
     /** Additional work after the setup of TV input. */
@@ -124,32 +123,29 @@
         TvSingletons tvSingletons = TvSingletons.getSingletons(context);
         final ChannelDataManager manager = tvSingletons.getChannelDataManager();
         manager.updateChannels(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        Channel firstChannelForInput = null;
-                        boolean browsableChanged = false;
-                        for (Channel channel : manager.getChannelList()) {
-                            if (channel.getInputId().equals(inputId)) {
-                                if (!channel.isBrowsable()) {
-                                    manager.updateBrowsable(channel.getId(), true, true);
-                                    browsableChanged = true;
-                                }
-                                if (firstChannelForInput == null) {
-                                    firstChannelForInput = channel;
-                                }
+                () -> {
+                    Channel firstChannelForInput = null;
+                    boolean browsableChanged = false;
+                    for (Channel channel : manager.getChannelList()) {
+                        if (channel.getInputId().equals(inputId)) {
+                            if (!channel.isBrowsable()) {
+                                manager.updateBrowsable(channel.getId(), true, true);
+                                browsableChanged = true;
+                            }
+                            if (firstChannelForInput == null) {
+                                firstChannelForInput = channel;
                             }
                         }
-                        if (firstChannelForInput != null) {
-                            Utils.setLastWatchedChannel(context, firstChannelForInput);
-                        }
-                        if (browsableChanged) {
-                            manager.notifyChannelBrowsableChanged();
-                            manager.applyUpdatedValuesToDb();
-                        }
-                        if (postRunnable != null) {
-                            postRunnable.run();
-                        }
+                    }
+                    if (firstChannelForInput != null) {
+                        Utils.setLastWatchedChannel(context, firstChannelForInput);
+                    }
+                    if (browsableChanged) {
+                        manager.notifyChannelBrowsableChanged();
+                        manager.applyUpdatedValuesToDb();
+                    }
+                    if (postRunnable != null) {
+                        postRunnable.run();
                     }
                 });
     }
@@ -332,7 +328,9 @@
         // A USB tuner device can be temporarily unplugged. We do not remove the USB tuner input
         // from the known inputs so that the input won't appear as a new input whenever the user
         // plugs in the USB tuner device again.
-        removedInputList.remove(mTunerInputId);
+        if (mOptionalTunerInputId.isPresent()) {
+            removedInputList.remove(mOptionalTunerInputId.get());
+        }
 
         if (!removedInputList.isEmpty()) {
             boolean inputPackageDeleted = false;
diff --git a/src/com/android/tv/util/SqlParams.java b/src/com/android/tv/util/SqlParams.java
index c4b803b..fa557ba 100644
--- a/src/com/android/tv/util/SqlParams.java
+++ b/src/com/android/tv/util/SqlParams.java
@@ -17,15 +17,16 @@
 package com.android.tv.util;
 
 import android.database.DatabaseUtils;
+import android.support.annotation.Nullable;
 import java.util.Arrays;
 
 /** Convenience class for SQL operations. */
 public class SqlParams {
     private String mTables;
-    private String mSelection;
-    private String[] mSelectionArgs;
+    private @Nullable String mSelection;
+    private @Nullable String[] mSelectionArgs;
 
-    public SqlParams(String tables, String selection, String... selectionArgs) {
+    public SqlParams(String tables, @Nullable String selection, @Nullable String... selectionArgs) {
         setTables(tables);
         setWhere(selection, selectionArgs);
     }
@@ -34,11 +35,11 @@
         return mTables;
     }
 
-    public String getSelection() {
+    public @Nullable String getSelection() {
         return mSelection;
     }
 
-    public String[] getSelectionArgs() {
+    public @Nullable String[] getSelectionArgs() {
         return mSelectionArgs;
     }
 
diff --git a/src/com/android/tv/util/TvInputManagerHelper.java b/src/com/android/tv/util/TvInputManagerHelper.java
index 625fb7b..cb7d985 100644
--- a/src/com/android/tv/util/TvInputManagerHelper.java
+++ b/src/com/android/tv/util/TvInputManagerHelper.java
@@ -19,21 +19,29 @@
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.database.ContentObserver;
 import android.graphics.drawable.Drawable;
 import android.hardware.hdmi.HdmiDeviceInfo;
 import android.media.tv.TvContentRatingSystemInfo;
 import android.media.tv.TvInputInfo;
 import android.media.tv.TvInputManager;
 import android.media.tv.TvInputManager.TvInputCallback;
+import android.net.Uri;
 import android.os.Handler;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
 import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
 import android.support.annotation.VisibleForTesting;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Log;
-import com.android.tv.TvFeatures;
 import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.compat.TvInputInfoCompat;
+import com.android.tv.common.dagger.annotations.ApplicationContext;
 import com.android.tv.common.util.CommonUtils;
+import com.android.tv.common.util.SystemProperties;
+import com.android.tv.features.TvFeatures;
 import com.android.tv.parental.ContentRatingsManager;
 import com.android.tv.parental.ParentalControlSettings;
 import com.android.tv.util.images.ImageCache;
@@ -46,7 +54,12 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import javax.inject.Inject;
+import javax.inject.Singleton;
 
+/** Helper class for {@link TvInputManager}. */
+@UiThread
+@Singleton
 public class TvInputManagerHelper {
     private static final String TAG = "TvInputManagerHelper";
     private static final boolean DEBUG = false;
@@ -117,6 +130,12 @@
     };
     private static final String META_LABEL_SORT_KEY = "input_sort_key";
 
+    private static final String TV_INPUT_ALLOW_3RD_PARTY_INPUTS = "tv_input_allow_3rd_party_inputs";
+
+    private static final String[] SYSTEM_INPUT_ID_BLACKLIST = {
+        "com.google.android.videos/" // Play Movies
+    };
+
     /** The default tv input priority to show. */
     private static final ArrayList<Integer> DEFAULT_TV_INPUT_PRIORITY = new ArrayList<>();
 
@@ -149,21 +168,24 @@
     private final PackageManager mPackageManager;
     protected final TvInputManagerInterface mTvInputManager;
     private final Map<String, Integer> mInputStateMap = new HashMap<>();
-    private final Map<String, TvInputInfo> mInputMap = new HashMap<>();
+    private final Map<String, TvInputInfoCompat> mInputMap = new HashMap<>();
     private final Map<String, String> mTvInputLabels = new ArrayMap<>();
     private final Map<String, String> mTvInputCustomLabels = new ArrayMap<>();
     private final Map<String, Boolean> mInputIdToPartnerInputMap = new HashMap<>();
 
     private final Map<String, CharSequence> mTvInputApplicationLabels = new ArrayMap<>();
     private final Map<String, Drawable> mTvInputApplicationIcons = new ArrayMap<>();
-    private final Map<String, Drawable> mTvInputAppliactionBanners = new ArrayMap<>();
+    private final Map<String, Drawable> mTvInputApplicationBanners = new ArrayMap<>();
+
+    private final ContentObserver mContentObserver;
 
     private final TvInputCallback mInternalCallback =
             new TvInputCallback() {
                 @Override
                 public void onInputStateChanged(String inputId, int state) {
                     if (DEBUG) Log.d(TAG, "onInputStateChanged " + inputId + " state=" + state);
-                    if (isInBlackList(inputId)) {
+                    TvInputInfo info = mInputMap.get(inputId).getTvInputInfo();
+                    if (info == null || isInputBlocked(info)) {
                         return;
                     }
                     mInputStateMap.put(inputId, state);
@@ -175,12 +197,12 @@
                 @Override
                 public void onInputAdded(String inputId) {
                     if (DEBUG) Log.d(TAG, "onInputAdded " + inputId);
-                    if (isInBlackList(inputId)) {
+                    TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
+                    if (info == null || isInputBlocked(info)) {
                         return;
                     }
-                    TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
                     if (info != null) {
-                        mInputMap.put(inputId, info);
+                        mInputMap.put(inputId, new TvInputInfoCompat(mContext, info));
                         CharSequence label = info.loadLabel(mContext);
                         // in tests the label may be missing just use the input id
                         mTvInputLabels.put(inputId, label != null ? label.toString() : inputId);
@@ -205,7 +227,7 @@
                     mTvInputCustomLabels.remove(inputId);
                     mTvInputApplicationLabels.remove(inputId);
                     mTvInputApplicationIcons.remove(inputId);
-                    mTvInputAppliactionBanners.remove(inputId);
+                    mTvInputApplicationBanners.remove(inputId);
                     mInputStateMap.remove(inputId);
                     mInputIdToPartnerInputMap.remove(inputId);
                     mContentRatingsManager.update();
@@ -219,11 +241,11 @@
                 @Override
                 public void onInputUpdated(String inputId) {
                     if (DEBUG) Log.d(TAG, "onInputUpdated " + inputId);
-                    if (isInBlackList(inputId)) {
+                    TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
+                    if (info == null || isInputBlocked(info)) {
                         return;
                     }
-                    TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
-                    mInputMap.put(inputId, info);
+                    mInputMap.put(inputId, new TvInputInfoCompat(mContext, info));
                     mTvInputLabels.put(inputId, info.loadLabel(mContext).toString());
                     CharSequence inputCustomLabel = info.loadCustomLabel(mContext);
                     if (inputCustomLabel != null) {
@@ -231,7 +253,7 @@
                     }
                     mTvInputApplicationLabels.remove(inputId);
                     mTvInputApplicationIcons.remove(inputId);
-                    mTvInputAppliactionBanners.remove(inputId);
+                    mTvInputApplicationBanners.remove(inputId);
                     for (TvInputCallback callback : mCallbacks) {
                         callback.onInputUpdated(inputId);
                     }
@@ -242,7 +264,10 @@
                 @Override
                 public void onTvInputInfoUpdated(TvInputInfo inputInfo) {
                     if (DEBUG) Log.d(TAG, "onTvInputInfoUpdated " + inputInfo);
-                    mInputMap.put(inputInfo.getId(), inputInfo);
+                    if (isInputBlocked(inputInfo)) {
+                        return;
+                    }
+                    mInputMap.put(inputInfo.getId(), new TvInputInfoCompat(mContext, inputInfo));
                     mTvInputLabels.put(inputInfo.getId(), inputInfo.loadLabel(mContext).toString());
                     CharSequence inputCustomLabel = inputInfo.loadCustomLabel(mContext);
                     if (inputCustomLabel != null) {
@@ -264,8 +289,10 @@
     private final ContentRatingsManager mContentRatingsManager;
     private final ParentalControlSettings mParentalControlSettings;
     private final Comparator<TvInputInfo> mTvInputInfoComparator;
+    private boolean mAllow3rdPartyInputs;
 
-    public TvInputManagerHelper(Context context) {
+    @Inject
+    public TvInputManagerHelper(@ApplicationContext Context context) {
         this(context, createTvInputManagerWrapper(context));
     }
 
@@ -285,6 +312,22 @@
         mContentRatingsManager = new ContentRatingsManager(context, tvInputManager);
         mParentalControlSettings = new ParentalControlSettings(context);
         mTvInputInfoComparator = new InputComparatorInternal(this);
+        mContentObserver =
+                new ContentObserver(mHandler) {
+                    @Override
+                    public void onChange(boolean selfChange, Uri uri) {
+                        String option = uri.getLastPathSegment();
+                        if (option == null || !option.equals(TV_INPUT_ALLOW_3RD_PARTY_INPUTS)) {
+                            return;
+                        }
+                        boolean previousSetting = mAllow3rdPartyInputs;
+                        updateAllow3rdPartyInputs();
+                        if (previousSetting == mAllow3rdPartyInputs) {
+                            return;
+                        }
+                        initInputMaps();
+                    }
+                };
     }
 
     public void start() {
@@ -297,30 +340,14 @@
         }
         if (DEBUG) Log.d(TAG, "start");
         mStarted = true;
+        mContext.getContentResolver()
+                .registerContentObserver(
+                        Settings.Global.getUriFor(TV_INPUT_ALLOW_3RD_PARTY_INPUTS),
+                        true,
+                        mContentObserver);
+        updateAllow3rdPartyInputs();
         mTvInputManager.registerCallback(mInternalCallback, mHandler);
-        mInputMap.clear();
-        mTvInputLabels.clear();
-        mTvInputCustomLabels.clear();
-        mTvInputApplicationLabels.clear();
-        mTvInputApplicationIcons.clear();
-        mTvInputAppliactionBanners.clear();
-        mInputStateMap.clear();
-        mInputIdToPartnerInputMap.clear();
-        for (TvInputInfo input : mTvInputManager.getTvInputList()) {
-            if (DEBUG) Log.d(TAG, "Input detected " + input);
-            String inputId = input.getId();
-            if (isInBlackList(inputId)) {
-                continue;
-            }
-            mInputMap.put(inputId, input);
-            int state = mTvInputManager.getInputState(inputId);
-            mInputStateMap.put(inputId, state);
-            mInputIdToPartnerInputMap.put(inputId, isPartnerInput(input));
-        }
-        SoftPreconditions.checkState(
-                mInputStateMap.size() == mInputMap.size(),
-                TAG,
-                "mInputStateMap not the same size as mInputMap");
+        initInputMaps();
         mContentRatingsManager.update();
     }
 
@@ -329,6 +356,7 @@
             return;
         }
         mTvInputManager.unregisterCallback(mInternalCallback);
+        mContext.getContentResolver().unregisterContentObserver(mContentObserver);
         mStarted = false;
         mInputStateMap.clear();
         mInputMap.clear();
@@ -336,8 +364,7 @@
         mTvInputCustomLabels.clear();
         mTvInputApplicationLabels.clear();
         mTvInputApplicationIcons.clear();
-        mTvInputAppliactionBanners.clear();
-        ;
+        mTvInputApplicationBanners.clear();
         mInputIdToPartnerInputMap.clear();
     }
 
@@ -355,6 +382,9 @@
                 continue;
             }
             TvInputInfo input = getTvInputInfo(pair.getKey());
+            if (input == null || isInputBlocked(input)) {
+                continue;
+            }
             if (tunerOnly && input.getType() != TvInputInfo.TYPE_TUNER) {
                 continue;
             }
@@ -460,12 +490,12 @@
 
     /** Gets the tv input application's banner. */
     public Drawable getTvInputApplicationBanner(String inputId) {
-        return mTvInputAppliactionBanners.get(inputId);
+        return mTvInputApplicationBanners.get(inputId);
     }
 
     /** Stores the tv input application's banner. */
     public void setTvInputApplicationBanner(String inputId, Drawable banner) {
-        mTvInputAppliactionBanners.put(inputId, banner);
+        mTvInputApplicationBanners.put(inputId, banner);
     }
 
     /** Returns if TV input exists with the input id. */
@@ -475,7 +505,14 @@
         return mStarted && !TextUtils.isEmpty(inputId) && mInputMap.get(inputId) != null;
     }
 
+    @Nullable
     public TvInputInfo getTvInputInfo(String inputId) {
+        TvInputInfoCompat inputInfo = getTvInputInfoCompat(inputId);
+        return inputInfo == null ? null : inputInfo.getTvInputInfo();
+    }
+
+    @Nullable
+    public TvInputInfoCompat getTvInputInfoCompat(String inputId) {
         SoftPreconditions.checkState(
                 mStarted, TAG, "getTvInputInfo() called before TvInputManagerHelper was started.");
         if (!mStarted) {
@@ -494,7 +531,7 @@
 
     public int getTunerTvInputSize() {
         int size = 0;
-        for (TvInputInfo input : mInputMap.values()) {
+        for (TvInputInfoCompat input : mInputMap.values()) {
             if (input.getType() == TvInputInfo.TYPE_TUNER) {
                 ++size;
             }
@@ -601,6 +638,61 @@
         return false;
     }
 
+    private void initInputMaps() {
+        mInputMap.clear();
+        mTvInputLabels.clear();
+        mTvInputCustomLabels.clear();
+        mTvInputApplicationLabels.clear();
+        mTvInputApplicationIcons.clear();
+        mTvInputApplicationBanners.clear();
+        mInputStateMap.clear();
+        mInputIdToPartnerInputMap.clear();
+        for (TvInputInfo input : mTvInputManager.getTvInputList()) {
+            if (DEBUG) {
+                Log.d(TAG, "Input detected " + input);
+            }
+            String inputId = input.getId();
+            if (isInputBlocked(input)) {
+                continue;
+            }
+            mInputMap.put(inputId, new TvInputInfoCompat(mContext, input));
+            int state = mTvInputManager.getInputState(inputId);
+            mInputStateMap.put(inputId, state);
+            mInputIdToPartnerInputMap.put(inputId, isPartnerInput(input));
+        }
+        SoftPreconditions.checkState(
+                mInputStateMap.size() == mInputMap.size(),
+                TAG,
+                "mInputStateMap not the same size as mInputMap");
+    }
+
+    private void updateAllow3rdPartyInputs() {
+        int setting;
+        try {
+            setting =
+                    Settings.Global.getInt(
+                            mContext.getContentResolver(), TV_INPUT_ALLOW_3RD_PARTY_INPUTS);
+        } catch (SettingNotFoundException e) {
+            mAllow3rdPartyInputs = SystemProperties.ALLOW_THIRD_PARTY_INPUTS.getValue();
+            return;
+        }
+        mAllow3rdPartyInputs = setting == 1;
+    }
+
+    private boolean isInputBlocked(TvInputInfo info) {
+        if (!mAllow3rdPartyInputs) {
+            if (!isSystemInput(info)) {
+                return true;
+            }
+            for (String id : SYSTEM_INPUT_ID_BLACKLIST) {
+                if (info.getId().startsWith(id)) {
+                    return true;
+                }
+            }
+        }
+        return isInBlackList(info.getId());
+    }
+
     /**
      * Default comparator for TvInputInfo.
      *
diff --git a/src/com/android/tv/util/TvProviderUtils.java b/src/com/android/tv/util/TvProviderUtils.java
new file mode 100644
index 0000000..6b5aaec
--- /dev/null
+++ b/src/com/android/tv/util/TvProviderUtils.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.util;
+
+import static java.lang.Boolean.TRUE;
+
+import android.content.Context;
+import android.media.tv.TvContract;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.StringDef;
+import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
+import android.util.Log;
+import com.android.tv.data.BaseProgram;
+import com.android.tv.features.PartnerFeatures;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/** A utility class related to TvProvider. */
+public final class TvProviderUtils {
+    private static final String TAG = "TvProviderUtils";
+
+    public static final String EXTRA_PROGRAM_COLUMN_SERIES_ID = BaseProgram.COLUMN_SERIES_ID;
+    public static final String EXTRA_PROGRAM_COLUMN_STATE = BaseProgram.COLUMN_STATE;
+
+    /** Possible extra columns in TV provider. */
+    @Retention(RetentionPolicy.SOURCE)
+    @StringDef({EXTRA_PROGRAM_COLUMN_SERIES_ID, EXTRA_PROGRAM_COLUMN_STATE})
+    public @interface TvProviderExtraColumn {}
+
+    private static boolean sProgramHasSeriesIdColumn;
+    private static boolean sRecordedProgramHasSeriesIdColumn;
+    private static boolean sRecordedProgramHasStateColumn;
+
+    /**
+     * Checks whether a table contains a series ID column.
+     *
+     * <p>This method is different from {@link #getProgramHasSeriesIdColumn()} and {@link
+     * #getRecordedProgramHasSeriesIdColumn()} because it may access to database, so it should be
+     * run in worker thread.
+     *
+     * @return {@code true} if the corresponding table contains a series ID column; {@code false}
+     *     otherwise.
+     */
+    @WorkerThread
+    public static synchronized boolean checkSeriesIdColumn(Context context, Uri uri) {
+        boolean canCreateColumn = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O);
+        if (!canCreateColumn) {
+            return false;
+        }
+        return (Utils.isRecordedProgramsUri(uri)
+                        && checkRecordedProgramTableSeriesIdColumn(context, uri))
+                || (Utils.isProgramsUri(uri) && checkProgramTableSeriesIdColumn(context, uri));
+    }
+
+    @WorkerThread
+    private static synchronized boolean checkProgramTableSeriesIdColumn(Context context, Uri uri) {
+        if (!sProgramHasSeriesIdColumn) {
+            if (getExistingColumns(context, uri).contains(EXTRA_PROGRAM_COLUMN_SERIES_ID)) {
+                sProgramHasSeriesIdColumn = true;
+            } else if (addColumnToTable(context, uri, EXTRA_PROGRAM_COLUMN_SERIES_ID)) {
+                sProgramHasSeriesIdColumn = true;
+            }
+        }
+        return sProgramHasSeriesIdColumn;
+    }
+
+    @WorkerThread
+    private static synchronized boolean checkRecordedProgramTableSeriesIdColumn(
+            Context context, Uri uri) {
+        if (!sRecordedProgramHasSeriesIdColumn) {
+            if (getExistingColumns(context, uri).contains(EXTRA_PROGRAM_COLUMN_SERIES_ID)) {
+                sRecordedProgramHasSeriesIdColumn = true;
+            } else if (addColumnToTable(context, uri, EXTRA_PROGRAM_COLUMN_SERIES_ID)) {
+                sRecordedProgramHasSeriesIdColumn = true;
+            }
+        }
+        return sRecordedProgramHasSeriesIdColumn;
+    }
+
+    /**
+     * Checks whether a table contains a state column.
+     *
+     * <p>This method is different from {@link #getRecordedProgramHasStateColumn()} because it may
+     * access to database, so it should be run in worker thread.
+     *
+     * @return {@code true} if the corresponding table contains a state column; {@code false}
+     *     otherwise.
+     */
+    @WorkerThread
+    public static synchronized boolean checkStateColumn(Context context, Uri uri) {
+        boolean canCreateColumn = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O);
+        if (!canCreateColumn) {
+            return false;
+        }
+        return (Utils.isRecordedProgramsUri(uri)
+                && checkRecordedProgramTableStateColumn(context, uri));
+    }
+
+    @WorkerThread
+    private static synchronized boolean checkRecordedProgramTableStateColumn(
+            Context context, Uri uri) {
+        if (!sRecordedProgramHasStateColumn) {
+            if (getExistingColumns(context, uri).contains(EXTRA_PROGRAM_COLUMN_STATE)) {
+                sRecordedProgramHasStateColumn = true;
+            } else if (addColumnToTable(context, uri, EXTRA_PROGRAM_COLUMN_STATE)) {
+                sRecordedProgramHasStateColumn = true;
+            }
+        }
+        return sRecordedProgramHasStateColumn;
+    }
+
+    public static synchronized boolean getProgramHasSeriesIdColumn() {
+        return TRUE.equals(sProgramHasSeriesIdColumn);
+    }
+
+    public static synchronized boolean getRecordedProgramHasSeriesIdColumn() {
+        return TRUE.equals(sRecordedProgramHasSeriesIdColumn);
+    }
+
+    public static synchronized boolean getRecordedProgramHasStateColumn() {
+        return TRUE.equals(sRecordedProgramHasStateColumn);
+    }
+
+    public static String[] addExtraColumnsToProjection(String[] projection,
+            @TvProviderExtraColumn String column) {
+        List<String> projectionList = new ArrayList<>(Arrays.asList(projection));
+        if (!projectionList.contains(column)) {
+            projectionList.add(column);
+        }
+        projection = projectionList.toArray(projection);
+        return projection;
+    }
+
+    /**
+     * Gets column names of a table
+     *
+     * @param uri the corresponding URI of the table
+     */
+    @VisibleForTesting
+    static Set<String> getExistingColumns(Context context, Uri uri) {
+        Bundle result = null;
+        try {
+            result =
+                    context.getContentResolver()
+                            .call(uri, TvContract.METHOD_GET_COLUMNS, uri.toString(), null);
+        } catch (Exception e) {
+            Log.e(TAG, "Error trying to get existing columns.", e);
+        }
+        if (result != null) {
+            String[] columns = result.getStringArray(TvContract.EXTRA_EXISTING_COLUMN_NAMES);
+            if (columns != null) {
+                return new HashSet<>(Arrays.asList(columns));
+            }
+        }
+        Log.e(TAG, "Query existing column names from " + uri + " returned null");
+        return Collections.emptySet();
+    }
+
+    /**
+     * Add a column to the table
+     *
+     * @return {@code true} if the column is added successfully; {@code false} otherwise.
+     */
+    private static boolean addColumnToTable(Context context, Uri contentUri, String columnName) {
+        Bundle extra = new Bundle();
+        extra.putCharSequence(TvContract.EXTRA_COLUMN_NAME, columnName);
+        extra.putCharSequence(TvContract.EXTRA_DATA_TYPE, "TEXT");
+        // If the add operation fails, the following just returns null without crashing.
+        Bundle allColumns = null;
+        try {
+            allColumns =
+                    context.getContentResolver()
+                            .call(
+                                    contentUri,
+                                    TvContract.METHOD_ADD_COLUMN,
+                                    contentUri.toString(),
+                                    extra);
+        } catch (Exception e) {
+            Log.e(TAG, "Error trying to add column.", e);
+        }
+        if (allColumns == null) {
+            Log.w(TAG, "Adding new column failed. Uri=" + contentUri);
+        }
+        return allColumns != null;
+    }
+
+    private TvProviderUtils() {}
+}
diff --git a/src/com/android/tv/util/TvTrackInfoUtils.java b/src/com/android/tv/util/TvTrackInfoUtils.java
index 0987450..4ec96c6 100644
--- a/src/com/android/tv/util/TvTrackInfoUtils.java
+++ b/src/com/android/tv/util/TvTrackInfoUtils.java
@@ -15,13 +15,28 @@
  */
 package com.android.tv.util;
 
+import android.content.Context;
 import android.media.tv.TvTrackInfo;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.tv.R;
 import java.util.Comparator;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Set;
 
 /** Static utilities for {@link TvTrackInfo}. */
 public class TvTrackInfoUtils {
 
+    private static final String TAG = "TvTrackInfoUtils";
+    private static final int AUDIO_CHANNEL_NONE = 0;
+    private static final int AUDIO_CHANNEL_MONO = 1;
+    private static final int AUDIO_CHANNEL_STEREO = 2;
+    private static final int AUDIO_CHANNEL_SURROUND_6 = 6;
+    private static final int AUDIO_CHANNEL_SURROUND_8 = 8;
+
     /**
      * Compares how closely two {@link android.media.tv.TvTrackInfo}s match {@code language}, {@code
      * channelCount} and {@code id} in that precedence.
@@ -34,40 +49,36 @@
      */
     public static Comparator<TvTrackInfo> createComparator(
             final String id, final String language, final int channelCount) {
-        return new Comparator<TvTrackInfo>() {
-
-            @Override
-            public int compare(TvTrackInfo lhs, TvTrackInfo rhs) {
-                if (lhs == rhs) {
-                    return 0;
-                }
-                if (lhs == null) {
-                    return -1;
-                }
-                if (rhs == null) {
-                    return 1;
-                }
-                // Assumes {@code null} language matches to any language since it means user hasn't
-                // selected any track before or selected a track without language information.
-                boolean lhsLangMatch =
-                        language == null || Utils.isEqualLanguage(lhs.getLanguage(), language);
-                boolean rhsLangMatch =
-                        language == null || Utils.isEqualLanguage(rhs.getLanguage(), language);
-                if (lhsLangMatch && rhsLangMatch) {
-                    boolean lhsCountMatch =
-                            lhs.getType() != TvTrackInfo.TYPE_AUDIO
-                                    || lhs.getAudioChannelCount() == channelCount;
-                    boolean rhsCountMatch =
-                            rhs.getType() != TvTrackInfo.TYPE_AUDIO
-                                    || rhs.getAudioChannelCount() == channelCount;
-                    if (lhsCountMatch && rhsCountMatch) {
-                        return Boolean.compare(lhs.getId().equals(id), rhs.getId().equals(id));
-                    } else {
-                        return Boolean.compare(lhsCountMatch, rhsCountMatch);
-                    }
+        return (TvTrackInfo lhs, TvTrackInfo rhs) -> {
+            if (Objects.equals(lhs, rhs)) {
+                return 0;
+            }
+            if (lhs == null) {
+                return -1;
+            }
+            if (rhs == null) {
+                return 1;
+            }
+            // Assumes {@code null} language matches to any language since it means user hasn't
+            // selected any track before or selected a track without language information.
+            boolean lhsLangMatch =
+                    language == null || Utils.isEqualLanguage(lhs.getLanguage(), language);
+            boolean rhsLangMatch =
+                    language == null || Utils.isEqualLanguage(rhs.getLanguage(), language);
+            if (lhsLangMatch && rhsLangMatch) {
+                boolean lhsCountMatch =
+                        lhs.getType() != TvTrackInfo.TYPE_AUDIO
+                                || lhs.getAudioChannelCount() == channelCount;
+                boolean rhsCountMatch =
+                        rhs.getType() != TvTrackInfo.TYPE_AUDIO
+                                || rhs.getAudioChannelCount() == channelCount;
+                if (lhsCountMatch && rhsCountMatch) {
+                    return Boolean.compare(lhs.getId().equals(id), rhs.getId().equals(id));
                 } else {
-                    return Boolean.compare(lhsLangMatch, rhsLangMatch);
+                    return Boolean.compare(lhsCountMatch, rhsCountMatch);
                 }
+            } else {
+                return Boolean.compare(lhsLangMatch, rhsLangMatch);
             }
         };
     }
@@ -96,5 +107,132 @@
         return best;
     }
 
+    public static boolean needToShowSampleRate(Context context, List<TvTrackInfo> tracks) {
+        Set<String> multiAudioStrings = new HashSet<>();
+        for (TvTrackInfo track : tracks) {
+            String multiAudioString = getMultiAudioString(context, track, false);
+            if (multiAudioStrings.contains(multiAudioString)) {
+                return true;
+            }
+            multiAudioStrings.add(multiAudioString);
+        }
+        return false;
+    }
+
+    public static String getMultiAudioString(
+            Context context, TvTrackInfo track, boolean showSampleRate) {
+        if (track.getType() != TvTrackInfo.TYPE_AUDIO) {
+            throw new IllegalArgumentException("Not an audio track: " + toString(track));
+        }
+        String language = context.getString(R.string.multi_audio_unknown_language);
+        if (!TextUtils.isEmpty(track.getLanguage())) {
+            language = new Locale(track.getLanguage()).getDisplayName();
+        } else {
+            Log.d(
+                TAG,
+                "No language information found for the audio track: "
+                + toString(track)
+            );
+        }
+
+        StringBuilder metadata = new StringBuilder();
+        switch (track.getAudioChannelCount()) {
+            case AUDIO_CHANNEL_NONE:
+                break;
+            case AUDIO_CHANNEL_MONO:
+                metadata.append(context.getString(R.string.multi_audio_channel_mono));
+                break;
+            case AUDIO_CHANNEL_STEREO:
+                metadata.append(context.getString(R.string.multi_audio_channel_stereo));
+                break;
+            case AUDIO_CHANNEL_SURROUND_6:
+                metadata.append(context.getString(R.string.multi_audio_channel_surround_6));
+                break;
+            case AUDIO_CHANNEL_SURROUND_8:
+                metadata.append(context.getString(R.string.multi_audio_channel_surround_8));
+                break;
+            default:
+                if (track.getAudioChannelCount() > 0) {
+                    metadata.append(
+                            context.getString(
+                                    R.string.multi_audio_channel_suffix,
+                                    track.getAudioChannelCount()));
+                } else {
+                    Log.d(
+                            TAG,
+                            "Invalid audio channel count ("
+                                    + track.getAudioChannelCount()
+                                    + ") found for the audio track: "
+                                    + toString(track));
+                }
+                break;
+        }
+        if (showSampleRate) {
+            int sampleRate = track.getAudioSampleRate();
+            if (sampleRate > 0) {
+                if (metadata.length() > 0) {
+                    metadata.append(", ");
+                }
+                int integerPart = sampleRate / 1000;
+                int tenths = (sampleRate % 1000) / 100;
+                metadata.append(integerPart);
+                if (tenths != 0) {
+                    metadata.append(".");
+                    metadata.append(tenths);
+                }
+                metadata.append("kHz");
+            }
+        }
+
+        if (metadata.length() == 0) {
+            return language;
+        }
+        return context.getString(
+                R.string.multi_audio_display_string_with_channel, language, metadata.toString());
+    }
+
+    private static String trackTypeToString(int trackType) {
+        switch (trackType) {
+            case TvTrackInfo.TYPE_AUDIO:
+                return "Audio";
+            case TvTrackInfo.TYPE_VIDEO:
+                return "Video";
+            case TvTrackInfo.TYPE_SUBTITLE:
+                return "Subtitle";
+            default:
+                return "Invalid Type";
+        }
+    }
+
+    public static String toString(TvTrackInfo info) {
+        int trackType = info.getType();
+        return "TvTrackInfo{"
+            + "type="
+            + trackTypeToString(trackType)
+            + ", id="
+            + info.getId()
+            + ", language="
+            + info.getLanguage()
+            + ", description="
+            + info.getDescription()
+            + (trackType == TvTrackInfo.TYPE_AUDIO
+                ?
+                (", audioChannelCount="
+                + info.getAudioChannelCount()
+                + ", audioSampleRate="
+                + info.getAudioSampleRate()) : "")
+            + (trackType == TvTrackInfo.TYPE_VIDEO
+                ?
+                (", videoWidth="
+                + info.getVideoWidth()
+                + ", videoHeight="
+                + info.getVideoHeight()
+                + ", videoFrameRate="
+                + info.getVideoFrameRate()
+                + ", videoPixelAspectRatio="
+                + info.getVideoPixelAspectRatio()) : "")
+            + "}";
+    }
+
     private TvTrackInfoUtils() {}
 }
diff --git a/src/com/android/tv/util/Utils.java b/src/com/android/tv/util/Utils.java
index a75bd44..5117373 100644
--- a/src/com/android/tv/util/Utils.java
+++ b/src/com/android/tv/util/Utils.java
@@ -29,7 +29,6 @@
 import android.media.tv.TvContract.Channels;
 import android.media.tv.TvContract.Programs.Genres;
 import android.media.tv.TvInputInfo;
-import android.media.tv.TvTrackInfo;
 import android.net.Uri;
 import android.os.Looper;
 import android.preference.PreferenceManager;
@@ -42,6 +41,7 @@
 import android.view.View;
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
+import com.android.tv.common.BaseSingletons;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.util.Clock;
 import com.android.tv.data.GenreItems;
@@ -99,12 +99,6 @@
     private static final int VIDEO_ULTRA_HD_WIDTH = 2048;
     private static final int VIDEO_ULTRA_HD_HEIGHT = 1536;
 
-    private static final int AUDIO_CHANNEL_NONE = 0;
-    private static final int AUDIO_CHANNEL_MONO = 1;
-    private static final int AUDIO_CHANNEL_STEREO = 2;
-    private static final int AUDIO_CHANNEL_SURROUND_6 = 6;
-    private static final int AUDIO_CHANNEL_SURROUND_8 = 8;
-
     private static final long RECORDING_FAILED_REASON_NONE = 0;
     private static final long HALF_MINUTE_MS = TimeUnit.SECONDS.toMillis(30);
     private static final long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1);
@@ -141,6 +135,7 @@
         return sb.toString();
     }
 
+    @Nullable
     @WorkerThread
     public static String getInputIdForChannel(Context context, long channelId) {
         if (channelId == Channel.INVALID_ID) {
@@ -153,6 +148,8 @@
             if (cursor != null && cursor.moveToNext()) {
                 return Utils.intern(cursor.getString(0));
             }
+        } catch (Exception e) {
+            Log.e(TAG, "Error get input id for channel", e);
         }
         return null;
     }
@@ -325,8 +322,17 @@
         Uri uri =
                 TvContract.buildProgramsUriForChannel(
                         TvContract.buildChannelUri(channelId), timeMs, timeMs);
-        try (Cursor cursor =
-                context.getContentResolver().query(uri, Program.PROJECTION, null, null, null)) {
+        ContentResolver resolver = context.getContentResolver();
+
+        String[] projection = Program.PROJECTION;
+        if (TvProviderUtils.checkSeriesIdColumn(context, TvContract.Programs.CONTENT_URI)) {
+            if (Utils.isProgramsUri(uri)) {
+                projection =
+                        TvProviderUtils.addExtraColumnsToProjection(
+                                projection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID);
+            }
+        }
+        try (Cursor cursor = resolver.query(uri, projection, null, null, null)) {
             if (cursor != null && cursor.moveToNext()) {
                 return Program.fromCursor(cursor);
             }
@@ -360,11 +366,10 @@
             Context context, long startUtcMillis, long endUtcMillis, boolean useShortFormat) {
         return getDurationString(
                 context,
-                System.currentTimeMillis(),
+                ((BaseSingletons) context.getApplicationContext()).getClock(),
                 startUtcMillis,
                 endUtcMillis,
-                useShortFormat,
-                0);
+                useShortFormat);
     }
 
     /**
@@ -400,7 +405,7 @@
             long startUtcMillis,
             long endUtcMillis,
             boolean useShortFormat,
-            int flag) {
+            int flags) {
         return getDurationString(
                 context,
                 startUtcMillis,
@@ -408,7 +413,7 @@
                 useShortFormat,
                 !isInGivenDay(baseMillis, startUtcMillis),
                 true,
-                flag);
+                flags);
     }
 
     /**
@@ -422,16 +427,20 @@
             boolean useShortFormat,
             boolean showDate,
             boolean showTime,
-            int flag) {
-        flag |=
+            int flags) {
+        flags |=
                 DateUtils.FORMAT_ABBREV_MONTH
                         | ((useShortFormat) ? DateUtils.FORMAT_NUMERIC_DATE : 0);
         SoftPreconditions.checkArgument(showTime || showDate);
         if (showTime) {
-            flag |= DateUtils.FORMAT_SHOW_TIME;
+            flags |= DateUtils.FORMAT_SHOW_TIME;
         }
         if (showDate) {
-            flag |= DateUtils.FORMAT_SHOW_DATE;
+            flags |= DateUtils.FORMAT_SHOW_DATE;
+        }
+        if (!showDate || (flags & DateUtils.FORMAT_SHOW_YEAR) == 0) {
+            // year is not shown unless DateUtils.FORMAT_SHOW_YEAR is set explicitly
+            flags |= DateUtils.FORMAT_NO_YEAR;
         }
         if (startUtcMillis != endUtcMillis && useShortFormat) {
             // Do special handling for 12:00 AM when checking if it's in the given day.
@@ -443,15 +452,15 @@
                 // Subtracting one day is needed because {@link DateUtils@formatDateRange}
                 // automatically shows date if the duration covers multiple days.
                 return DateUtils.formatDateRange(
-                        context, startUtcMillis, endUtcMillis - TimeUnit.DAYS.toMillis(1), flag);
+                        context, startUtcMillis, endUtcMillis - TimeUnit.DAYS.toMillis(1), flags);
             }
         }
         // Workaround of b/28740989.
         // Add 1 msec to endUtcMillis to avoid DateUtils' bug with a duration of 12:00AM~12:00AM.
-        String dateRange = DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis, flag);
+        String dateRange = DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis, flags);
         return startUtcMillis == endUtcMillis || dateRange.contains("–")
                 ? dateRange
-                : DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis + 1, flag);
+                : DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis + 1, flags);
     }
 
     /**
@@ -572,86 +581,6 @@
         return "";
     }
 
-    public static boolean needToShowSampleRate(Context context, List<TvTrackInfo> tracks) {
-        Set<String> multiAudioStrings = new HashSet<>();
-        for (TvTrackInfo track : tracks) {
-            String multiAudioString = getMultiAudioString(context, track, false);
-            if (multiAudioStrings.contains(multiAudioString)) {
-                return true;
-            }
-            multiAudioStrings.add(multiAudioString);
-        }
-        return false;
-    }
-
-    public static String getMultiAudioString(
-            Context context, TvTrackInfo track, boolean showSampleRate) {
-        if (track.getType() != TvTrackInfo.TYPE_AUDIO) {
-            throw new IllegalArgumentException("Not an audio track: " + track);
-        }
-        String language = context.getString(R.string.multi_audio_unknown_language);
-        if (!TextUtils.isEmpty(track.getLanguage())) {
-            language = new Locale(track.getLanguage()).getDisplayName();
-        } else {
-            Log.d(TAG, "No language information found for the audio track: " + track);
-        }
-
-        StringBuilder metadata = new StringBuilder();
-        switch (track.getAudioChannelCount()) {
-            case AUDIO_CHANNEL_NONE:
-                break;
-            case AUDIO_CHANNEL_MONO:
-                metadata.append(context.getString(R.string.multi_audio_channel_mono));
-                break;
-            case AUDIO_CHANNEL_STEREO:
-                metadata.append(context.getString(R.string.multi_audio_channel_stereo));
-                break;
-            case AUDIO_CHANNEL_SURROUND_6:
-                metadata.append(context.getString(R.string.multi_audio_channel_surround_6));
-                break;
-            case AUDIO_CHANNEL_SURROUND_8:
-                metadata.append(context.getString(R.string.multi_audio_channel_surround_8));
-                break;
-            default:
-                if (track.getAudioChannelCount() > 0) {
-                    metadata.append(
-                            context.getString(
-                                    R.string.multi_audio_channel_suffix,
-                                    track.getAudioChannelCount()));
-                } else {
-                    Log.d(
-                            TAG,
-                            "Invalid audio channel count ("
-                                    + track.getAudioChannelCount()
-                                    + ") found for the audio track: "
-                                    + track);
-                }
-                break;
-        }
-        if (showSampleRate) {
-            int sampleRate = track.getAudioSampleRate();
-            if (sampleRate > 0) {
-                if (metadata.length() > 0) {
-                    metadata.append(", ");
-                }
-                int integerPart = sampleRate / 1000;
-                int tenths = (sampleRate % 1000) / 100;
-                metadata.append(integerPart);
-                if (tenths != 0) {
-                    metadata.append(".");
-                    metadata.append(tenths);
-                }
-                metadata.append("kHz");
-            }
-        }
-
-        if (metadata.length() == 0) {
-            return language;
-        }
-        return context.getString(
-                R.string.multi_audio_display_string_with_channel, language, metadata.toString());
-    }
-
     public static boolean isEqualLanguage(String lang1, String lang2) {
         if (lang1 == null) {
             return lang2 == null;
@@ -708,7 +637,6 @@
         if (fullFormat) {
             return new Date(timeMillis).toString();
         } else {
-            long currentTime = System.currentTimeMillis();
             return (String)
                     DateUtils.formatSameDayTime(
                             timeMillis,
@@ -815,8 +743,11 @@
 
     /** Checks whether the input is internal or not. */
     public static boolean isInternalTvInput(Context context, String inputId) {
-        return context.getPackageName()
-                .equals(ComponentName.unflattenFromString(inputId).getPackageName());
+        ComponentName unflattenInputId = ComponentName.unflattenFromString(inputId);
+        if (unflattenInputId == null) {
+            return false;
+        }
+        return context.getPackageName().equals(unflattenInputId.getPackageName());
     }
 
     /** Returns the TV input for the given {@code program}. */
diff --git a/src/com/android/tv/util/images/BitmapUtils.java b/src/com/android/tv/util/images/BitmapUtils.java
index d6bd5a3..3952450 100644
--- a/src/com/android/tv/util/images/BitmapUtils.java
+++ b/src/com/android/tv/util/images/BitmapUtils.java
@@ -20,13 +20,16 @@
 import android.content.Context;
 import android.database.sqlite.SQLiteException;
 import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
 import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
 import android.graphics.PorterDuff;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.net.TrafficStats;
 import android.net.Uri;
 import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.text.TextUtils;
 import android.util.Log;
 import com.android.tv.common.util.NetworkTrafficTags;
@@ -88,6 +91,19 @@
                 calculateInSampleSize(bm.getWidth(), bm.getHeight(), maxWidth, maxHeight));
     }
 
+    @Nullable
+    public static Bitmap drawableToBitmap(Drawable drawable) {
+        if (drawable == null) {
+            return null;
+        }
+        Bitmap bm = Bitmap.createBitmap(
+                drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Config.ARGB_8888);
+        Canvas canvas = new Canvas(bm);
+        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+        drawable.draw(canvas);
+        return bm;
+    }
+
     /** Decode large sized bitmap into requested size. */
     public static ScaledBitmapInfo decodeSampledBitmapFromUriString(
             Context context, String uriString, int reqWidth, int reqHeight) {
diff --git a/src/com/android/tv/util/images/ImageLoader.java b/src/com/android/tv/util/images/ImageLoader.java
index e844e2c..d2ad0eb 100644
--- a/src/com/android/tv/util/images/ImageLoader.java
+++ b/src/com/android/tv/util/images/ImageLoader.java
@@ -24,7 +24,6 @@
 import android.os.AsyncTask;
 import android.os.Handler;
 import android.os.Looper;
-import android.support.annotation.MainThread;
 import android.support.annotation.Nullable;
 import android.support.annotation.UiThread;
 import android.support.annotation.WorkerThread;
@@ -145,22 +144,14 @@
             final Context appContext = context.getApplicationContext();
             getMainHandler()
                     .post(
-                            new Runnable() {
-                                @Override
-                                @MainThread
-                                public void run() {
-                                    // Calling from the main thread prevents a
-                                    // ConcurrentModificationException
-                                    // in LoadBitmapTask.onPostExecute
+                            () ->
                                     doLoadBitmap(
                                             appContext,
                                             uriString,
                                             maxWidth,
                                             maxHeight,
                                             null,
-                                            AsyncTask.SERIAL_EXECUTOR);
-                                }
-                            });
+                                            AsyncTask.SERIAL_EXECUTOR));
         }
     }
 
@@ -423,14 +414,12 @@
         @Override
         public ScaledBitmapInfo doGetBitmapInBackground() {
             Drawable drawable = mInfo.loadIcon(mAppContext);
-            if (!(drawable instanceof BitmapDrawable)) {
-                return null;
-            }
-            Bitmap original = ((BitmapDrawable) drawable).getBitmap();
-            if (original == null) {
-                return null;
-            }
-            return BitmapUtils.createScaledBitmapInfo(getKey(), original, mMaxWidth, mMaxHeight);
+            Bitmap bm = drawable instanceof BitmapDrawable
+                    ? ((BitmapDrawable) drawable).getBitmap()
+                    : BitmapUtils.drawableToBitmap(drawable);
+            return bm == null
+                    ? null
+                    : BitmapUtils.createScaledBitmapInfo(getKey(), bm, mMaxWidth, mMaxHeight);
         }
 
         /** Returns key of TV input logo. */
diff --git a/tests/common/Android.mk b/tests/common/Android.mk
index 3ab16c0..7a111d0 100644
--- a/tests/common/Android.mk
+++ b/tests/common/Android.mk
@@ -8,11 +8,11 @@
 
 LOCAL_STATIC_JAVA_LIBRARIES := \
     android-support-annotations \
-    android-support-test \
-    guava \
+    androidx.test.runner \
+    androidx.test.rules \
+    tv-guava-android-jar \
     mockito-target \
-    platform-robolectric-3.6.2-prebuilt \
-    truth-0-36-prebuilt-jar \
+    tv-lib-truth \
     ub-uiautomator \
 
 # Link tv-common as shared library to avoid the problem of initialization of the constants
diff --git a/tests/common/AndroidManifest.xml b/tests/common/AndroidManifest.xml
index 8afd8dc..3a769a8 100644
--- a/tests/common/AndroidManifest.xml
+++ b/tests/common/AndroidManifest.xml
@@ -18,6 +18,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.tv.testing"
           android:versionCode="1">
-  <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="21"/>
+  <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
     <application />
 </manifest>
diff --git a/tests/common/src/com/android/tv/testing/DbTestingUtils.java b/tests/common/src/com/android/tv/testing/DbTestingUtils.java
index 53e26ca..e71a714 100644
--- a/tests/common/src/com/android/tv/testing/DbTestingUtils.java
+++ b/tests/common/src/com/android/tv/testing/DbTestingUtils.java
@@ -29,7 +29,7 @@
         while (cursor.moveToNext()) {
             List<String> row = new ArrayList<>(colCount);
             for (int i = 0; i < colCount; i++) {
-                row.add(cursor.getString(i));
+                row.add(cursor.isNull(i) ? "null" : cursor.getString(i));
             }
             result.add(row);
         }
diff --git a/tests/common/src/com/android/tv/testing/EpgTestData.java b/tests/common/src/com/android/tv/testing/EpgTestData.java
index 49a9218..362f336 100644
--- a/tests/common/src/com/android/tv/testing/EpgTestData.java
+++ b/tests/common/src/com/android/tv/testing/EpgTestData.java
@@ -30,18 +30,19 @@
 /** EPG data for use in tests. */
 public abstract class EpgTestData {
 
-    public static final android.support.media.tv.Channel CHANNEL_10 =
-            new android.support.media.tv.Channel.Builder()
+    public static final androidx.tvprovider.media.tv.Channel CHANNEL_10 =
+            new androidx.tvprovider.media.tv.Channel.Builder()
                     .setDisplayName("Channel TEN")
                     .setDisplayNumber("10")
+                    .setNetworkAffiliation("Channel 10 Network Affiliation")
                     .build();
-    public static final android.support.media.tv.Channel CHANNEL_11 =
-            new android.support.media.tv.Channel.Builder()
+    public static final androidx.tvprovider.media.tv.Channel CHANNEL_11 =
+            new androidx.tvprovider.media.tv.Channel.Builder()
                     .setDisplayName("Channel Eleven")
                     .setDisplayNumber("11")
                     .build();
-    public static final android.support.media.tv.Channel CHANNEL_90_2 =
-            new android.support.media.tv.Channel.Builder()
+    public static final androidx.tvprovider.media.tv.Channel CHANNEL_90_2 =
+            new androidx.tvprovider.media.tv.Channel.Builder()
                     .setDisplayName("Channel Ninety dot Two")
                     .setDisplayNumber("90.2")
                     .build();
@@ -162,21 +163,23 @@
         loadData(testSingletonApp.fakeClock, testSingletonApp.epgReader);
     }
 
-    private static Iterable<Channel> toTvChannels(android.support.media.tv.Channel... channels) {
+    private static Iterable<Channel> toTvChannels(
+            androidx.tvprovider.media.tv.Channel... channels) {
         return Iterables.transform(
                 ImmutableList.copyOf(channels),
-                new Function<android.support.media.tv.Channel, Channel>() {
+                new Function<androidx.tvprovider.media.tv.Channel, Channel>() {
                     @Override
-                    public Channel apply(android.support.media.tv.Channel original) {
+                    public Channel apply(androidx.tvprovider.media.tv.Channel original) {
                         return toTvChannel(original);
                     }
                 });
     }
 
-    public static Channel toTvChannel(android.support.media.tv.Channel original) {
+    public static Channel toTvChannel(androidx.tvprovider.media.tv.Channel original) {
         return new ChannelImpl.Builder()
                 .setDisplayName(original.getDisplayName())
                 .setDisplayNumber(original.getDisplayNumber())
+                .setNetworkAffiliation(original.getNetworkAffiliation())
                 // TODO implement the reset
                 .build();
     }
diff --git a/tests/common/src/com/android/tv/testing/FakeEpgReader.java b/tests/common/src/com/android/tv/testing/FakeEpgReader.java
index 710ada5..fb35c65 100644
--- a/tests/common/src/com/android/tv/testing/FakeEpgReader.java
+++ b/tests/common/src/com/android/tv/testing/FakeEpgReader.java
@@ -37,6 +37,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
 /** Fake {@link EpgReader} for testing. */
@@ -93,7 +94,15 @@
             if (match != null) {
                 ChannelImpl updatedChannel = new ChannelImpl.Builder(match).build();
                 updatedChannel.setLogoUri(channel.getLogoUri());
-                result.add(EpgChannel.createEpgChannel(updatedChannel, channel.getDisplayNumber()));
+                boolean dbUpdateNeeded = false;
+                if (!Objects.equals(
+                        channel.getNetworkAffiliation(), updatedChannel.getNetworkAffiliation())) {
+                    dbUpdateNeeded = true;
+                    updatedChannel.setNetworkAffiliation(channel.getNetworkAffiliation());
+                }
+                result.add(
+                        EpgChannel.createEpgChannel(
+                                updatedChannel, channel.getDisplayNumber(), dbUpdateNeeded));
             }
         }
         return result;
diff --git a/tests/common/src/com/android/tv/testing/FakeRemoteConfig.java b/tests/common/src/com/android/tv/testing/FakeRemoteConfig.java
deleted file mode 100644
index 89e6a0a..0000000
--- a/tests/common/src/com/android/tv/testing/FakeRemoteConfig.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.tv.testing;
-
-import android.text.TextUtils;
-import com.android.tv.common.config.api.RemoteConfig;
-import java.util.HashMap;
-import java.util.Map;
-
-/** Fake {@link RemoteConfig} suitable for testing. */
-public class FakeRemoteConfig implements RemoteConfig {
-    public final Map<String, String> values = new HashMap();
-
-    @Override
-    public void fetch(OnRemoteConfigUpdatedListener listener) {}
-
-    @Override
-    public String getString(String key) {
-        return values.get(key);
-    }
-
-    @Override
-    public boolean getBoolean(String key) {
-        String value = values.get(key);
-        return TextUtils.isEmpty(value) ? false : Boolean.valueOf(key);
-    }
-
-    @Override
-    public long getLong(String key) {
-        return getLong(key, 0);
-    }
-
-    @Override
-    public long getLong(String key, long defaultValue) {
-        if (values.containsKey(key)) {
-            String value = values.get(key);
-            return TextUtils.isEmpty(value) ? defaultValue : Long.valueOf(value);
-        }
-        return defaultValue;
-    }
-}
diff --git a/tests/common/src/com/android/tv/testing/FakeTvProvider.java b/tests/common/src/com/android/tv/testing/FakeTvProvider.java
index 24c26f3..20903c6 100644
--- a/tests/common/src/com/android/tv/testing/FakeTvProvider.java
+++ b/tests/common/src/com/android/tv/testing/FakeTvProvider.java
@@ -44,16 +44,16 @@
 import android.preference.PreferenceManager;
 import android.provider.BaseColumns;
 import android.support.annotation.VisibleForTesting;
-import android.support.media.tv.TvContractCompat;
-import android.support.media.tv.TvContractCompat.BaseTvColumns;
-import android.support.media.tv.TvContractCompat.Channels;
-import android.support.media.tv.TvContractCompat.PreviewPrograms;
-import android.support.media.tv.TvContractCompat.Programs;
-import android.support.media.tv.TvContractCompat.Programs.Genres;
-import android.support.media.tv.TvContractCompat.RecordedPrograms;
-import android.support.media.tv.TvContractCompat.WatchNextPrograms;
 import android.text.TextUtils;
 import android.util.Log;
+import androidx.tvprovider.media.tv.TvContractCompat;
+import androidx.tvprovider.media.tv.TvContractCompat.BaseTvColumns;
+import androidx.tvprovider.media.tv.TvContractCompat.Channels;
+import androidx.tvprovider.media.tv.TvContractCompat.PreviewPrograms;
+import androidx.tvprovider.media.tv.TvContractCompat.Programs;
+import androidx.tvprovider.media.tv.TvContractCompat.Programs.Genres;
+import androidx.tvprovider.media.tv.TvContractCompat.RecordedPrograms;
+import androidx.tvprovider.media.tv.TvContractCompat.WatchNextPrograms;
 import com.android.tv.util.SqlParams;
 import java.io.ByteArrayOutputStream;
 import java.io.FileNotFoundException;
diff --git a/tests/common/src/com/android/tv/testing/TestSingletonApp.java b/tests/common/src/com/android/tv/testing/TestSingletonApp.java
index f55ed8d..f1a98ff 100644
--- a/tests/common/src/com/android/tv/testing/TestSingletonApp.java
+++ b/tests/common/src/com/android/tv/testing/TestSingletonApp.java
@@ -17,9 +17,6 @@
 package com.android.tv.testing;
 
 import android.app.Application;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
 import android.media.tv.TvInputManager;
 import android.os.AsyncTask;
 import com.android.tv.InputSessionManager;
@@ -28,9 +25,14 @@
 import com.android.tv.analytics.Analytics;
 import com.android.tv.analytics.Tracker;
 import com.android.tv.common.BaseApplication;
-import com.android.tv.common.config.api.RemoteConfig;
 import com.android.tv.common.experiments.ExperimentLoader;
+import com.android.tv.common.flags.impl.DefaultBackendKnobsFlags;
+import com.android.tv.common.flags.impl.DefaultCloudEpgFlags;
+import com.android.tv.common.flags.impl.DefaultConcurrentDvrPlaybackFlags;
+import com.android.tv.common.flags.impl.DefaultTunerFlags;
+import com.android.tv.common.flags.impl.DefaultUiFlags;
 import com.android.tv.common.recording.RecordingStorageStatusManager;
+import com.android.tv.common.singletons.HasSingletons;
 import com.android.tv.common.util.Clock;
 import com.android.tv.data.ChannelDataManager;
 import com.android.tv.data.PreviewDataManager;
@@ -43,21 +45,27 @@
 import com.android.tv.dvr.DvrWatchedPositionManager;
 import com.android.tv.dvr.recorder.RecordingScheduler;
 import com.android.tv.perf.PerformanceMonitor;
-import com.android.tv.perf.StubPerformanceMonitor;
+import com.android.tv.perf.stub.StubPerformanceMonitor;
 import com.android.tv.testing.dvr.DvrDataManagerInMemoryImpl;
 import com.android.tv.testing.testdata.TestData;
-import com.android.tv.tuner.TunerInputController;
+import com.android.tv.tuner.singletons.TunerSingletons;
+import com.android.tv.tuner.source.TsDataSourceManager;
+import com.android.tv.tuner.source.TunerTsStreamerManager;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactory;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactoryImpl;
+import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
 import com.android.tv.util.SetupUtils;
 import com.android.tv.util.TvInputManagerHelper;
 import com.android.tv.util.account.AccountHelper;
+import com.google.common.base.Optional;
 import java.util.concurrent.Executor;
 import javax.inject.Provider;
 
 /** Test application for Live TV. */
-public class TestSingletonApp extends Application implements TvSingletons {
+public class TestSingletonApp extends Application
+        implements TvSingletons, TunerSingletons, HasSingletons<TvSingletons> {
     public final FakeClock fakeClock = FakeClock.createWithCurrentTime();
     public final FakeEpgReader epgReader = new FakeEpgReader(fakeClock);
-    public final FakeRemoteConfig remoteConfig = new FakeRemoteConfig();
     public final FakeEpgFetcher epgFetcher = new FakeEpgFetcher();
 
     public FakeTvInputManagerHelper tvInputManagerHelper;
@@ -66,19 +74,27 @@
     public DvrDataManager mDvrDataManager;
 
     private final Provider<EpgReader> mEpgReaderProvider = SingletonProvider.create(epgReader);
-    private TunerInputController mTunerInputController;
+    private final Optional<BuiltInTunerManager> mBuiltInTunerManagerOptional = Optional.absent();
+    private final DefaultBackendKnobsFlags mBackendKnobs = new DefaultBackendKnobsFlags();
+    private final DefaultCloudEpgFlags mCloudEpgFlags = new DefaultCloudEpgFlags();
+    private final DefaultUiFlags mUiFlags = new DefaultUiFlags();
+    private final DefaultConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags =
+            new DefaultConcurrentDvrPlaybackFlags();
+    private final TsDataSourceManager.Factory mTsDataSourceManagerFactory =
+            new TsDataSourceManager.Factory(() -> new TunerTsStreamerManager(null));
+    private final TunerSessionFactoryImpl mTunerSessionFactory =
+            new TunerSessionFactoryImpl(
+                    new DefaultTunerFlags(),
+                    mConcurrentDvrPlaybackFlags,
+                    mTsDataSourceManagerFactory);
     private PerformanceMonitor mPerformanceMonitor;
     private ChannelDataManager mChannelDataManager;
 
     @Override
     public void onCreate() {
         super.onCreate();
-        mTunerInputController =
-                new TunerInputController(
-                        ComponentName.unflattenFromString(getEmbeddedTunerInputId()));
-
         tvInputManagerHelper = new FakeTvInputManagerHelper(this);
-        setupUtils = SetupUtils.createForTvSingletons(this);
+        setupUtils = new SetupUtils(this, mBuiltInTunerManagerOptional);
         tvInputManagerHelper.start();
         mChannelDataManager = new ChannelDataManager(this, tvInputManagerHelper);
         mChannelDataManager.start();
@@ -154,7 +170,7 @@
 
     @Override
     public InputSessionManager getInputSessionManager() {
-        return null;
+        return new InputSessionManager(this);
     }
 
     @Override
@@ -183,8 +199,8 @@
     }
 
     @Override
-    public TunerInputController getTunerInputController() {
-        return mTunerInputController;
+    public Optional<BuiltInTunerManager> getBuiltInTunerManager() {
+        return mBuiltInTunerManagerOptional;
     }
 
     @Override
@@ -213,16 +229,6 @@
     }
 
     @Override
-    public RemoteConfig getRemoteConfig() {
-        return remoteConfig;
-    }
-
-    @Override
-    public Intent getTunerSetupIntent(Context context) {
-        return null;
-    }
-
-    @Override
     public boolean isRunningInMainProcess() {
         return false;
     }
@@ -244,4 +250,38 @@
     public Executor getDbExecutor() {
         return AsyncTask.SERIAL_EXECUTOR;
     }
+
+    @Override
+    public DefaultBackendKnobsFlags getBackendKnobs() {
+        return mBackendKnobs;
+    }
+
+    @Override
+    public DefaultCloudEpgFlags getCloudEpgFlags() {
+        return mCloudEpgFlags;
+    }
+
+    @Override
+    public DefaultUiFlags getUiFlags() {
+        return mUiFlags;
+    }
+
+    @Override
+    public BuildType getBuildType() {
+        return BuildType.ENG;
+    }
+
+    @Override
+    public DefaultConcurrentDvrPlaybackFlags getConcurrentDvrPlaybackFlags() {
+        return mConcurrentDvrPlaybackFlags;
+    }
+
+    public TunerSessionFactory getTunerSessionFactory() {
+        return mTunerSessionFactory;
+    }
+
+    @Override
+    public TvSingletons singletons() {
+        return this;
+    }
 }
diff --git a/tests/common/src/com/android/tv/testing/activities/BaseMainActivityTestCase.java b/tests/common/src/com/android/tv/testing/activities/BaseMainActivityTestCase.java
index 666f818..495ff20 100644
--- a/tests/common/src/com/android/tv/testing/activities/BaseMainActivityTestCase.java
+++ b/tests/common/src/com/android/tv/testing/activities/BaseMainActivityTestCase.java
@@ -15,12 +15,12 @@
  */
 package com.android.tv.testing.activities;
 
-import static android.support.test.InstrumentationRegistry.getInstrumentation;
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
 import android.content.Context;
 import android.os.SystemClock;
-import android.support.test.rule.ActivityTestRule;
 import android.text.TextUtils;
+import androidx.test.rule.ActivityTestRule;
 import com.android.tv.MainActivity;
 import com.android.tv.data.ChannelDataManager;
 import com.android.tv.data.api.Channel;
diff --git a/tests/common/src/com/android/tv/testing/data/ProgramInfo.java b/tests/common/src/com/android/tv/testing/data/ProgramInfo.java
index 6d80142..3e7b608 100644
--- a/tests/common/src/com/android/tv/testing/data/ProgramInfo.java
+++ b/tests/common/src/com/android/tv/testing/data/ProgramInfo.java
@@ -22,7 +22,7 @@
 import android.media.tv.TvContract;
 import com.android.tv.testing.R;
 import com.android.tv.testing.utils.Utils;
-import java.util.Arrays;
+import com.google.common.collect.ImmutableList;
 import java.util.Objects;
 import java.util.concurrent.TimeUnit;
 
@@ -100,7 +100,7 @@
     public final String description;
     public final long durationMs;
     public final String genre;
-    public final TvContentRating[] contentRatings;
+    public final ImmutableList<TvContentRating> contentRatings;
     public final String resourceUri;
 
     public static ProgramInfo fromCursor(Cursor c) {
@@ -129,7 +129,7 @@
             String posterArtUri,
             String description,
             long durationMs,
-            TvContentRating[] contentRatings,
+            ImmutableList<TvContentRating> contentRatings,
             String genre,
             String resourceUri) {
         this.title = title;
@@ -248,7 +248,7 @@
                 && Objects.equals(posterArtUri, that.posterArtUri)
                 && Objects.equals(description, that.description)
                 && Objects.equals(genre, that.genre)
-                && Arrays.equals(contentRatings, that.contentRatings)
+                && Objects.equals(contentRatings, that.contentRatings)
                 && Objects.equals(resourceUri, that.resourceUri);
     }
 
@@ -265,7 +265,7 @@
         private String mPosterArtUri = GEN_POSTER;
         private String mDescription;
         private long mDurationMs = GEN_DURATION;
-        private TvContentRating[] mContentRatings;
+        private ImmutableList<TvContentRating> mContentRatings;
         private String mGenre = GEN_GENRE;
         private String mResourceUri;
 
@@ -304,7 +304,7 @@
             return this;
         }
 
-        public Builder setContentRatings(TvContentRating[] contentRatings) {
+        public Builder setContentRatings(ImmutableList<TvContentRating> contentRatings) {
             mContentRatings = contentRatings;
             return this;
         }
diff --git a/tests/common/src/com/android/tv/testing/dvr/DvrDataManagerInMemoryImpl.java b/tests/common/src/com/android/tv/testing/dvr/DvrDataManagerInMemoryImpl.java
index b8a055c..66707fb 100644
--- a/tests/common/src/com/android/tv/testing/dvr/DvrDataManagerInMemoryImpl.java
+++ b/tests/common/src/com/android/tv/testing/dvr/DvrDataManagerInMemoryImpl.java
@@ -204,11 +204,7 @@
                     recordedProgram.getId() == RecordedProgram.ID_NOT_SET,
                     TAG,
                     "expected id of " + RecordedProgram.ID_NOT_SET + " but was " + recordedProgram);
-            recordedProgram =
-                    RecordedProgram
-                            .buildFrom(recordedProgram)
-                            .setId(mNextId.incrementAndGet())
-                            .build();
+            recordedProgram = recordedProgram.withId(mNextId.incrementAndGet());
         }
         mRecordedPrograms.put(recordedProgram.getId(), recordedProgram);
         notifyRecordedProgramsAdded(recordedProgram);
diff --git a/tests/common/src/com/android/tv/testing/robo/ContentProviders.java b/tests/common/src/com/android/tv/testing/robo/ContentProviders.java
deleted file mode 100644
index aaaa11d..0000000
--- a/tests/common/src/com/android/tv/testing/robo/ContentProviders.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.tv.testing.robo;
-
-import android.content.ContentProvider;
-import android.content.pm.ProviderInfo;
-import org.robolectric.Robolectric;
-import org.robolectric.android.controller.ContentProviderController;
-import org.robolectric.shadows.ShadowContentResolver;
-
-/** Static utilities for using content providers in tests. */
-public final class ContentProviders {
-
-    /** Builds creates and register a ContentProvider with the given authority. */
-    public static <T extends ContentProvider> T register(Class<T> providerClass, String authority) {
-        ProviderInfo info = new ProviderInfo();
-        info.authority = authority;
-        ContentProviderController<T> contentProviderController =
-                Robolectric.buildContentProvider(providerClass);
-        T provider = contentProviderController.create(info).get();
-        provider.onCreate();
-        ShadowContentResolver.registerProviderInternal(authority, provider);
-        return provider;
-    }
-
-    private ContentProviders() {}
-}
diff --git a/tests/common/src/com/android/tv/testing/robo/RobotTestAppHelper.java b/tests/common/src/com/android/tv/testing/robo/RobotTestAppHelper.java
deleted file mode 100644
index 9eb7929..0000000
--- a/tests/common/src/com/android/tv/testing/robo/RobotTestAppHelper.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-package com.android.tv.testing.robo;
-
-import android.media.tv.TvContract;
-import com.android.tv.testing.FakeTvProvider;
-import com.android.tv.testing.TestSingletonApp;
-import com.android.tv.testing.testdata.TestData;
-import java.util.concurrent.TimeUnit;
-import org.robolectric.Robolectric;
-
-/** Static utilities for using {@link TestSingletonApp} in roboletric tests. */
-public final class RobotTestAppHelper {
-
-    public static void loadTestData(TestSingletonApp app, TestData testData) {
-        ContentProviders.register(FakeTvProvider.class, TvContract.AUTHORITY);
-        app.loadTestData(testData, TimeUnit.DAYS.toMillis(1));
-        Robolectric.flushBackgroundThreadScheduler();
-        Robolectric.flushForegroundThreadScheduler();
-    }
-
-    private RobotTestAppHelper() {}
-}
diff --git a/tests/common/src/com/android/tv/testing/shadows/ShadowMediaSession.java b/tests/common/src/com/android/tv/testing/shadows/ShadowMediaSession.java
deleted file mode 100644
index 5a2c41e..0000000
--- a/tests/common/src/com/android/tv/testing/shadows/ShadowMediaSession.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.tv.testing.shadows;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.media.MediaMetadata;
-import android.media.session.MediaSession;
-import android.media.session.PlaybackState;
-import org.robolectric.annotation.Implementation;
-import org.robolectric.annotation.Implements;
-
-/** Shadow {@link MediaSession}. */
-@Implements(MediaSession.class)
-public class ShadowMediaSession {
-
-    public MediaSession.Callback mCallback;
-    public PendingIntent mMediaButtonReceiver;
-    public PendingIntent mSessionActivity;
-    public PlaybackState mPlaybackState;
-    public MediaMetadata mMediaMetadata;
-    public int mFlags;
-    public boolean mActive;
-    public boolean mReleased;
-
-    /** Stand-in for the MediaSession constructor with the same parameters. */
-    public void __constructor__(Context context, String tag, int userID) {
-        // This empty method prevents the real MediaSession constructor from being called.
-    }
-
-    @Implementation
-    public void setCallback(MediaSession.Callback callback) {
-        mCallback = callback;
-    }
-
-    @Implementation
-    public void setMediaButtonReceiver(PendingIntent mbr) {
-        mMediaButtonReceiver = mbr;
-    }
-
-    @Implementation
-    public void setSessionActivity(PendingIntent activity) {
-        mSessionActivity = activity;
-    }
-
-    @Implementation
-    public void setPlaybackState(PlaybackState state) {
-        mPlaybackState = state;
-    }
-
-    @Implementation
-    public void setMetadata(MediaMetadata metadata) {
-        mMediaMetadata = metadata;
-    }
-
-    @Implementation
-    public void setFlags(int flags) {
-        mFlags = flags;
-    }
-
-    @Implementation
-    public boolean isActive() {
-        return mActive;
-    }
-
-    @Implementation
-    public void setActive(boolean active) {
-        mActive = active;
-    }
-
-    @Implementation
-    public void release() {
-        mReleased = true;
-    }
-}
diff --git a/tests/func/Android.mk b/tests/func/Android.mk
index 855e8eb..53c869e 100644
--- a/tests/func/Android.mk
+++ b/tests/func/Android.mk
@@ -10,7 +10,7 @@
 LOCAL_PACKAGE_NAME := TVFuncTests
 
 LOCAL_STATIC_JAVA_LIBRARIES := \
-    android-support-test \
+    androidx.test.runner \
     tv-test-common \
     ub-uiautomator \
 
diff --git a/tests/func/AndroidManifest.xml b/tests/func/AndroidManifest.xml
index 708dc22..3d7d775 100644
--- a/tests/func/AndroidManifest.xml
+++ b/tests/func/AndroidManifest.xml
@@ -18,10 +18,10 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.tv.tests.ui" >
 
-    <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="21" />
+    <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23" />
 
     <instrumentation
-        android:name="android.support.test.runner.AndroidJUnitRunner"
+        android:name="androidx.test.runner.AndroidJUnitRunner"
         android:label="Live Channel Functional Tests"
         android:targetPackage="com.android.tv" />
 
diff --git a/tests/func/src/com/android/tv/tests/ui/ChannelBannerViewTest.java b/tests/func/src/com/android/tv/tests/ui/ChannelBannerViewTest.java
index 600b52b..c06c859 100644
--- a/tests/func/src/com/android/tv/tests/ui/ChannelBannerViewTest.java
+++ b/tests/func/src/com/android/tv/tests/ui/ChannelBannerViewTest.java
@@ -16,8 +16,8 @@
 
 package com.android.tv.tests.ui;
 
-import android.support.test.filters.MediumTest;
 import android.support.test.uiautomator.Until;
+import androidx.test.filters.MediumTest;
 import com.android.tv.R;
 import com.android.tv.testing.uihelper.Constants;
 import org.junit.Before;
diff --git a/tests/func/src/com/android/tv/tests/ui/ChannelSourcesTest.java b/tests/func/src/com/android/tv/tests/ui/ChannelSourcesTest.java
index 53e27f1..2467de2 100644
--- a/tests/func/src/com/android/tv/tests/ui/ChannelSourcesTest.java
+++ b/tests/func/src/com/android/tv/tests/ui/ChannelSourcesTest.java
@@ -15,9 +15,9 @@
  */
 package com.android.tv.tests.ui;
 
-import android.support.test.filters.MediumTest;
 import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.Until;
+import androidx.test.filters.MediumTest;
 import com.android.tv.R;
 import com.android.tv.testing.uihelper.ByResource;
 import org.junit.Before;
diff --git a/tests/func/src/com/android/tv/tests/ui/LiveChannelsAppTest.java b/tests/func/src/com/android/tv/tests/ui/LiveChannelsAppTest.java
index 1a5ceb4..ac2aad4 100644
--- a/tests/func/src/com/android/tv/tests/ui/LiveChannelsAppTest.java
+++ b/tests/func/src/com/android/tv/tests/ui/LiveChannelsAppTest.java
@@ -16,9 +16,9 @@
 
 package com.android.tv.tests.ui;
 
-import android.support.test.filters.MediumTest;
 import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.Until;
+import androidx.test.filters.MediumTest;
 import com.android.tv.R;
 import com.android.tv.testing.testinput.ChannelStateData;
 import com.android.tv.testing.testinput.TvTestInputConstants;
diff --git a/tests/func/src/com/android/tv/tests/ui/LiveChannelsTestController.java b/tests/func/src/com/android/tv/tests/ui/LiveChannelsTestController.java
index 03d30ca..fa3335d 100644
--- a/tests/func/src/com/android/tv/tests/ui/LiveChannelsTestController.java
+++ b/tests/func/src/com/android/tv/tests/ui/LiveChannelsTestController.java
@@ -23,7 +23,6 @@
 import android.content.res.Resources;
 import android.os.Build;
 import android.os.SystemClock;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.Configurator;
 import android.support.test.uiautomator.SearchCondition;
@@ -31,6 +30,7 @@
 import android.support.test.uiautomator.Until;
 import android.view.InputDevice;
 import android.view.KeyEvent;
+import androidx.test.InstrumentationRegistry;
 import com.android.tv.testing.data.ChannelInfo;
 import com.android.tv.testing.testinput.ChannelStateData;
 import com.android.tv.testing.testinput.TestInputControlConnection;
diff --git a/tests/func/src/com/android/tv/tests/ui/ParentalControlsTest.java b/tests/func/src/com/android/tv/tests/ui/ParentalControlsTest.java
index ee039d7..bff0e7d 100644
--- a/tests/func/src/com/android/tv/tests/ui/ParentalControlsTest.java
+++ b/tests/func/src/com/android/tv/tests/ui/ParentalControlsTest.java
@@ -19,10 +19,10 @@
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertTrue;
 
-import android.support.test.filters.MediumTest;
 import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.Until;
+import androidx.test.filters.MediumTest;
 import com.android.tv.R;
 import com.android.tv.testing.uihelper.ByResource;
 import com.android.tv.testing.uihelper.DialogHelper;
diff --git a/tests/func/src/com/android/tv/tests/ui/PlayControlsRowViewTest.java b/tests/func/src/com/android/tv/tests/ui/PlayControlsRowViewTest.java
index 7c98278..efc7ecf 100644
--- a/tests/func/src/com/android/tv/tests/ui/PlayControlsRowViewTest.java
+++ b/tests/func/src/com/android/tv/tests/ui/PlayControlsRowViewTest.java
@@ -22,11 +22,11 @@
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertNotNull;
 
-import android.support.test.filters.SmallTest;
 import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.Until;
 import android.view.KeyEvent;
+import androidx.test.filters.SmallTest;
 import com.android.tv.R;
 import com.android.tv.testing.testinput.TvTestInputConstants;
 import com.android.tv.testing.uihelper.Constants;
diff --git a/tests/func/src/com/android/tv/tests/ui/ProgramGuideTest.java b/tests/func/src/com/android/tv/tests/ui/ProgramGuideTest.java
index 4adf448..0a6a85d 100644
--- a/tests/func/src/com/android/tv/tests/ui/ProgramGuideTest.java
+++ b/tests/func/src/com/android/tv/tests/ui/ProgramGuideTest.java
@@ -15,8 +15,8 @@
  */
 package com.android.tv.tests.ui;
 
-import android.support.test.filters.MediumTest;
 import android.support.test.uiautomator.Until;
+import androidx.test.filters.MediumTest;
 import com.android.tv.guide.ProgramGuide;
 import com.android.tv.testing.uihelper.Constants;
 import org.junit.Rule;
diff --git a/tests/func/src/com/android/tv/tests/ui/TimeoutTest.java b/tests/func/src/com/android/tv/tests/ui/TimeoutTest.java
index 4b6befe..73e869f 100644
--- a/tests/func/src/com/android/tv/tests/ui/TimeoutTest.java
+++ b/tests/func/src/com/android/tv/tests/ui/TimeoutTest.java
@@ -15,8 +15,8 @@
  */
 package com.android.tv.tests.ui;
 
-import android.support.test.filters.LargeTest;
 import android.support.test.uiautomator.Until;
+import androidx.test.filters.LargeTest;
 import com.android.tv.R;
 import com.android.tv.testing.uihelper.Constants;
 import org.junit.Ignore;
diff --git a/tests/func/src/com/android/tv/tests/ui/dvr/DvrLibraryTest.java b/tests/func/src/com/android/tv/tests/ui/dvr/DvrLibraryTest.java
index d0ebed9..8998b45 100644
--- a/tests/func/src/com/android/tv/tests/ui/dvr/DvrLibraryTest.java
+++ b/tests/func/src/com/android/tv/tests/ui/dvr/DvrLibraryTest.java
@@ -19,11 +19,11 @@
 import static com.android.tv.testing.uihelper.UiDeviceAsserts.assertWaitUntilFocused;
 
 import android.os.Build;
-import android.support.test.filters.MediumTest;
-import android.support.test.filters.SdkSuppress;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.Until;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SdkSuppress;
 import com.android.tv.R;
 import com.android.tv.testing.uihelper.ByResource;
 import com.android.tv.testing.uihelper.Constants;
diff --git a/tests/func/src/com/android/tv/tests/ui/sidepanel/CustomizeChannelListFragmentTest.java b/tests/func/src/com/android/tv/tests/ui/sidepanel/CustomizeChannelListFragmentTest.java
index 09b855e..d035874 100644
--- a/tests/func/src/com/android/tv/tests/ui/sidepanel/CustomizeChannelListFragmentTest.java
+++ b/tests/func/src/com/android/tv/tests/ui/sidepanel/CustomizeChannelListFragmentTest.java
@@ -21,11 +21,11 @@
 import static junit.framework.Assert.assertTrue;
 
 import android.graphics.Point;
-import android.support.test.filters.MediumTest;
 import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.Direction;
 import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.Until;
+import androidx.test.filters.MediumTest;
 import com.android.tv.R;
 import com.android.tv.testing.uihelper.Constants;
 import com.android.tv.tests.ui.LiveChannelsTestController;
diff --git a/tests/input/AndroidManifest.xml b/tests/input/AndroidManifest.xml
index 9b5df2f..fa52946 100644
--- a/tests/input/AndroidManifest.xml
+++ b/tests/input/AndroidManifest.xml
@@ -18,7 +18,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.tv.testinput">
 
-    <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="23"/>
+    <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
 
      <!-- Required to update or read existing channel and program information in TvProvider. -->
     <uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" />
diff --git a/tests/input/jank.sh b/tests/input/jank.sh
index c6311a4..0710a4f 100644
--- a/tests/input/jank.sh
+++ b/tests/input/jank.sh
@@ -17,8 +17,8 @@
 # text fixture setup for unit tests
 
 
-echo "text fixture setup for func tests"
+echo "text fixture setup for jank tests"
 
 am instrument \
   -e testSetupMode jank \
-  -w com.android.tv.testinput/.instrument.TestSetupInstrumentation
\ No newline at end of file
+  -w com.android.tv.testinput/.instrument.TestSetupInstrumentation
diff --git a/tests/input/tools/get_test_logos.sh b/tests/input/tools/get_test_logos.sh
index 4dd87a3..649c51a 100755
--- a/tests/input/tools/get_test_logos.sh
+++ b/tests/input/tools/get_test_logos.sh
@@ -25,7 +25,7 @@
    bus cafe camping car-dealer car-rental
    car-repair casino caution cemetery-grave cemetery-tomb
    cinema civic-building computer corporate courthouse
-   fire flag floral helicopter home
+   fire flag helicopter home
    info landslide legal location locomotive
    medical mobile motorcycle music parking
    pet petrol phone picnic postal
diff --git a/tests/jank/Android.mk b/tests/jank/Android.mk
index 1b67ac3..7df77ea 100644
--- a/tests/jank/Android.mk
+++ b/tests/jank/Android.mk
@@ -10,7 +10,7 @@
 LOCAL_PACKAGE_NAME := TVJankTests
 
 LOCAL_STATIC_JAVA_LIBRARIES := \
-    android-support-test \
+    androidx.test.runner \
     tv-test-common \
     ub-janktesthelper \
     ub-uiautomator \
diff --git a/tests/jank/AndroidManifest.xml b/tests/jank/AndroidManifest.xml
index 5ea72b4..7c0997a 100644
--- a/tests/jank/AndroidManifest.xml
+++ b/tests/jank/AndroidManifest.xml
@@ -18,10 +18,10 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.tv.tests.jank" >
 
-    <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="21" />
+    <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23" />
 
     <instrumentation
-            android:name="android.support.test.runner.AndroidJUnitRunner"
+            android:name="androidx.test.runner.AndroidJUnitRunner"
             android:label="Live Channel Jank Tests"
             android:targetPackage="com.android.tv" />
 
diff --git a/tests/jank/README.md b/tests/jank/README.md
new file mode 100644
index 0000000..c40eb22
--- /dev/null
+++ b/tests/jank/README.md
@@ -0,0 +1,32 @@
+# Jank tests for Live Channels
+
+
+## AOSP instructions
+
+To run the jank tests
+
+```bash
+echo "Compiling"
+m -j LiveTv TVTestInput TVJankTests
+echo  "Installing"
+adb install -r ${OUT}/system/priv-app/LiveTv/LiveTv.apk
+adb install -r ${OUT}/system/app/TVTestInput/TVTestInput.apk
+adb install -r ${OUT}/testcases/TVJankTests/TVJankTests.apk
+echo "Setting up test input"
+adb shell am instrument \
+  -e testSetupMode jank \
+  -w com.android.tv.testinput/.instrument.TestSetupInstrumentation
+echo "Running the test"
+adb shell am instrument \
+  -w com.android.tv.tests.jank/android.support.test.runner.AndroidJUnitRunner
+
+```
+
+If it is your first time installing LiveTv you will need to do
+
+```bash
+adb root
+adb remount
+adb push ${OUT}/system/priv-app/LiveTv/LiveTv.apk /system/priv-app/LiveTv/LiveTv.apk
+adb reboot
+```
\ No newline at end of file
diff --git a/tests/jank/src/com/android/tv/tests/jank/ChannelZappingJankTest.java b/tests/jank/src/com/android/tv/tests/jank/ChannelZappingJankTest.java
index eee2328..02ca673 100644
--- a/tests/jank/src/com/android/tv/tests/jank/ChannelZappingJankTest.java
+++ b/tests/jank/src/com/android/tv/tests/jank/ChannelZappingJankTest.java
@@ -15,9 +15,9 @@
  */
 package com.android.tv.tests.jank;
 
-import android.support.test.filters.MediumTest;
 import android.support.test.jank.GfxMonitor;
 import android.support.test.jank.JankTest;
+import androidx.test.filters.MediumTest;
 
 /** Jank tests for channel zapping. */
 @MediumTest
diff --git a/tests/jank/src/com/android/tv/tests/jank/MenuJankTest.java b/tests/jank/src/com/android/tv/tests/jank/MenuJankTest.java
index ea80eb3..6b0dcd0 100644
--- a/tests/jank/src/com/android/tv/tests/jank/MenuJankTest.java
+++ b/tests/jank/src/com/android/tv/tests/jank/MenuJankTest.java
@@ -15,9 +15,9 @@
  */
 package com.android.tv.tests.jank;
 
-import android.support.test.filters.MediumTest;
 import android.support.test.jank.GfxMonitor;
 import android.support.test.jank.JankTest;
+import androidx.test.filters.MediumTest;
 import com.android.tv.testing.uihelper.MenuHelper;
 
 /** Jank tests for the program guide. */
diff --git a/tests/jank/src/com/android/tv/tests/jank/ProgramGuideJankTest.java b/tests/jank/src/com/android/tv/tests/jank/ProgramGuideJankTest.java
index 57d38ba..da2eb9c 100644
--- a/tests/jank/src/com/android/tv/tests/jank/ProgramGuideJankTest.java
+++ b/tests/jank/src/com/android/tv/tests/jank/ProgramGuideJankTest.java
@@ -17,10 +17,10 @@
 
 import static com.android.tv.testing.uihelper.UiDeviceAsserts.assertWaitForCondition;
 
-import android.support.test.filters.MediumTest;
 import android.support.test.jank.GfxMonitor;
 import android.support.test.jank.JankTest;
 import android.support.test.uiautomator.Until;
+import androidx.test.filters.MediumTest;
 import com.android.tv.R;
 import com.android.tv.testing.uihelper.ByResource;
 import com.android.tv.testing.uihelper.Constants;
diff --git a/tests/unit/Android.mk b/tests/unit/Android.mk
index a425bcf..5ea7ccd 100644
--- a/tests/unit/Android.mk
+++ b/tests/unit/Android.mk
@@ -8,7 +8,7 @@
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
 LOCAL_STATIC_JAVA_LIBRARIES := \
-    android-support-test \
+    androidx.test.runner \
     mockito-target \
     tv-test-common \
 
diff --git a/tests/unit/AndroidManifest.xml b/tests/unit/AndroidManifest.xml
index 9134a1c..c7d2f52 100644
--- a/tests/unit/AndroidManifest.xml
+++ b/tests/unit/AndroidManifest.xml
@@ -18,10 +18,10 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.tv.tests" >
 
-    <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="23" />
+    <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23" />
 
     <instrumentation
-        android:name="android.support.test.runner.AndroidJUnitRunner"
+        android:name="androidx.test.runner.AndroidJUnitRunner"
         android:label="Live Channel Unit Tests"
         android:targetPackage="com.android.tv" />
 
diff --git a/tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java b/tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java
index abadde3..4b85eaa 100644
--- a/tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java
+++ b/tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java
@@ -20,9 +20,9 @@
 import static com.android.tv.TimeShiftManager.REQUEST_TIMEOUT_MS;
 import static com.google.common.truth.Truth.assertWithMessage;
 
-import android.support.test.annotation.UiThreadTest;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
 import com.android.tv.testing.activities.BaseMainActivityTestCase;
 import org.junit.Before;
 import org.junit.Test;
diff --git a/tests/unit/src/com/android/tv/MainActivityTest.java b/tests/unit/src/com/android/tv/MainActivityTest.java
index c5df21a..f6223ec 100644
--- a/tests/unit/src/com/android/tv/MainActivityTest.java
+++ b/tests/unit/src/com/android/tv/MainActivityTest.java
@@ -15,14 +15,14 @@
  */
 package com.android.tv;
 
-import static android.support.test.InstrumentationRegistry.getInstrumentation;
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
 import android.view.View;
 import android.widget.TextView;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
 import com.android.tv.data.api.Channel;
 import com.android.tv.testing.activities.BaseMainActivityTestCase;
 import com.android.tv.testing.testinput.TvTestInputConstants;
diff --git a/tests/unit/src/com/android/tv/TimeShiftManagerTest.java b/tests/unit/src/com/android/tv/TimeShiftManagerTest.java
index cb52304..7adee38 100644
--- a/tests/unit/src/com/android/tv/TimeShiftManagerTest.java
+++ b/tests/unit/src/com/android/tv/TimeShiftManagerTest.java
@@ -24,8 +24,8 @@
 import static com.android.tv.TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND;
 import static com.google.common.truth.Truth.assertWithMessage;
 
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
 import com.android.tv.testing.activities.BaseMainActivityTestCase;
 import org.junit.Before;
 import org.junit.Test;
diff --git a/tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java b/tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java
index 96c1f7a..71ccaf3 100644
--- a/tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java
+++ b/tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java
@@ -16,8 +16,8 @@
 
 package com.android.tv.data;
 
-import static android.support.test.InstrumentationRegistry.getInstrumentation;
-import static android.support.test.InstrumentationRegistry.getTargetContext;
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+import static androidx.test.InstrumentationRegistry.getTargetContext;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
@@ -25,14 +25,14 @@
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
+import android.content.res.AssetFileDescriptor;
 import android.database.ContentObserver;
 import android.database.Cursor;
 import android.media.tv.TvContract;
 import android.media.tv.TvContract.Channels;
 import android.net.Uri;
 import android.os.AsyncTask;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import android.os.Bundle;
 import android.test.MoreAsserts;
 import android.test.mock.MockContentProvider;
 import android.test.mock.MockContentResolver;
@@ -40,10 +40,13 @@
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.SparseArray;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
 import com.android.tv.data.api.Channel;
 import com.android.tv.testing.constants.Constants;
 import com.android.tv.testing.data.ChannelInfo;
 import com.android.tv.util.TvInputManagerHelper;
+import java.io.FileNotFoundException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -98,9 +101,20 @@
                                         Mockito.mock(TvInputManagerHelper.class);
                                 Mockito.when(mockHelper.hasTvInputInfo(Matchers.anyString()))
                                         .thenReturn(true);
+                                Context mockContext = Mockito.mock(Context.class);
+                                Mockito.when(mockContext.getContentResolver())
+                                        .thenReturn(mContentResolver);
+                                Mockito.when(mockContext.checkSelfPermission(Matchers.anyString()))
+                                  .thenAnswer(
+                                          invocation -> {
+                                              Object[] args = invocation.getArguments();
+                                              return getTargetContext()
+                                                      .checkSelfPermission(((String) args[0]));
+                                          });
+
                                 mChannelDataManager =
                                         new ChannelDataManager(
-                                                getTargetContext(),
+                                                mockContext,
                                                 mockHelper,
                                                 AsyncTask.SERIAL_EXECUTOR,
                                                 mContentResolver);
@@ -417,6 +431,15 @@
             }
         }
 
+        @Override
+        public AssetFileDescriptor openTypedAssetFile(Uri url, String mimeType, Bundle opts) {
+            try {
+                return getTargetContext().getContentResolver().openAssetFileDescriptor(url, "r");
+            } catch (FileNotFoundException e) {
+                return null;
+            }
+        }
+
         /**
          * Implementation of {@link ContentProvider#query}. This assumes that {@link
          * ChannelDataManager} queries channels with empty {@code selection}. (i.e. channels are
diff --git a/tests/unit/src/com/android/tv/data/ChannelImplTest.java b/tests/unit/src/com/android/tv/data/ChannelImplTest.java
index b791a7e..86cfab6 100644
--- a/tests/unit/src/com/android/tv/data/ChannelImplTest.java
+++ b/tests/unit/src/com/android/tv/data/ChannelImplTest.java
@@ -25,8 +25,8 @@
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
 import com.android.tv.data.api.Channel;
 import com.android.tv.testing.ComparatorTester;
 import com.android.tv.util.TvInputManagerHelper;
diff --git a/tests/unit/src/com/android/tv/data/TvInputNewComparatorTest.java b/tests/unit/src/com/android/tv/data/TvInputNewComparatorTest.java
deleted file mode 100644
index 8e892cc..0000000
--- a/tests/unit/src/com/android/tv/data/TvInputNewComparatorTest.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tv.data;
-
-import android.content.pm.ResolveInfo;
-import android.media.tv.TvInputInfo;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-import android.util.Pair;
-import com.android.tv.testing.ComparatorTester;
-import com.android.tv.testing.utils.TestUtils;
-import com.android.tv.util.SetupUtils;
-import com.android.tv.util.TvInputManagerHelper;
-import java.util.Comparator;
-import java.util.LinkedHashMap;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Matchers;
-import org.mockito.Mockito;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-/** Test for {@link TvInputNewComparator} */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class TvInputNewComparatorTest {
-    @Test
-    public void testComparator() throws Exception {
-        LinkedHashMap<String, Pair<Boolean, Boolean>> inputIdToNewInput = new LinkedHashMap<>();
-        inputIdToNewInput.put("2_new_input", new Pair<>(true, false));
-        inputIdToNewInput.put("4_new_input", new Pair<>(true, false));
-        inputIdToNewInput.put("4_old_input", new Pair<>(false, false));
-        inputIdToNewInput.put("0_old_input", new Pair<>(false, true));
-        inputIdToNewInput.put("1_old_input", new Pair<>(false, true));
-        inputIdToNewInput.put("3_old_input", new Pair<>(false, true));
-
-        SetupUtils setupUtils = Mockito.mock(SetupUtils.class);
-        Mockito.when(setupUtils.isNewInput(Matchers.anyString()))
-                .thenAnswer(
-                        new Answer<Boolean>() {
-                            @Override
-                            public Boolean answer(InvocationOnMock invocation) throws Throwable {
-                                String inputId = (String) invocation.getArguments()[0];
-                                return inputIdToNewInput.get(inputId).first;
-                            }
-                        });
-        Mockito.when(setupUtils.isSetupDone(Matchers.anyString()))
-                .thenAnswer(
-                        new Answer<Boolean>() {
-                            @Override
-                            public Boolean answer(InvocationOnMock invocation) throws Throwable {
-                                String inputId = (String) invocation.getArguments()[0];
-                                return inputIdToNewInput.get(inputId).second;
-                            }
-                        });
-        TvInputManagerHelper inputManager = Mockito.mock(TvInputManagerHelper.class);
-        Mockito.when(inputManager.getDefaultTvInputInfoComparator())
-                .thenReturn(
-                        new Comparator<TvInputInfo>() {
-                            @Override
-                            public int compare(TvInputInfo lhs, TvInputInfo rhs) {
-                                return lhs.getId().compareTo(rhs.getId());
-                            }
-                        });
-        TvInputNewComparator comparator = new TvInputNewComparator(setupUtils, inputManager);
-        ComparatorTester<TvInputInfo> comparatorTester =
-                ComparatorTester.withoutEqualsTest(comparator);
-        ResolveInfo resolveInfo = TestUtils.createResolveInfo("test", "test");
-        for (String id : inputIdToNewInput.keySet()) {
-            // Put mock resolveInfo to prevent NPE in {@link TvInputInfo#toString}
-            TvInputInfo info1 =
-                    TestUtils.createTvInputInfo(
-                            resolveInfo, id, "test1", TvInputInfo.TYPE_TUNER, false);
-            TvInputInfo info2 =
-                    TestUtils.createTvInputInfo(
-                            resolveInfo, id, "test2", TvInputInfo.TYPE_DISPLAY_PORT, true);
-            TvInputInfo info3 =
-                    TestUtils.createTvInputInfo(
-                            resolveInfo, id, "test", TvInputInfo.TYPE_HDMI, true);
-            comparatorTester.addComparableGroup(info1, info2, info3);
-        }
-        comparatorTester.test();
-    }
-}
diff --git a/tests/unit/src/com/android/tv/data/WatchedHistoryManagerTest.java b/tests/unit/src/com/android/tv/data/WatchedHistoryManagerTest.java
deleted file mode 100644
index 43bfde0..0000000
--- a/tests/unit/src/com/android/tv/data/WatchedHistoryManagerTest.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tv.data;
-
-import static android.support.test.InstrumentationRegistry.getTargetContext;
-import static com.google.common.truth.Truth.assertThat;
-
-import android.os.Looper;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
-import com.android.tv.data.WatchedHistoryManager.WatchedRecord;
-import java.util.concurrent.TimeUnit;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Test for {@link com.android.tv.data.WatchedHistoryManagerTest}
- *
- * <p>This is a medium test because it load files which accessing SharedPreferences.
- */
-@MediumTest
-@RunWith(AndroidJUnit4.class)
-public class WatchedHistoryManagerTest {
-    // Wait time for expected success.
-    private static final int MAX_HISTORY_SIZE = 100;
-
-    private WatchedHistoryManager mWatchedHistoryManager;
-    private TestWatchedHistoryManagerListener mListener;
-
-    @Before
-    public void setUp() {
-        if (Looper.myLooper() == null) {
-            Looper.prepare();
-        }
-        mWatchedHistoryManager = new WatchedHistoryManager(getTargetContext(), MAX_HISTORY_SIZE);
-        mListener = new TestWatchedHistoryManagerListener();
-        mWatchedHistoryManager.setListener(mListener);
-    }
-
-    private void startAndWaitForComplete() throws InterruptedException {
-        mWatchedHistoryManager.start();
-        assertThat(mListener.mLoadFinished).isTrue();
-    }
-
-    @Test
-    public void testIsLoaded() throws InterruptedException {
-        startAndWaitForComplete();
-        assertThat(mWatchedHistoryManager.isLoaded()).isTrue();
-    }
-
-    @Test
-    public void testLogChannelViewStop() throws InterruptedException {
-        startAndWaitForComplete();
-        long fakeId = 100000000;
-        long time = System.currentTimeMillis();
-        long duration = TimeUnit.MINUTES.toMillis(10);
-        ChannelImpl channel = new ChannelImpl.Builder().setId(fakeId).build();
-        mWatchedHistoryManager.logChannelViewStop(channel, time, duration);
-
-        WatchedRecord record = mWatchedHistoryManager.getRecord(0);
-        WatchedRecord recordFromSharedPreferences =
-                mWatchedHistoryManager.getRecordFromSharedPreferences(0);
-        assertThat(fakeId).isEqualTo(record.channelId);
-        assertThat(time - duration).isEqualTo(record.watchedStartTime);
-        assertThat(duration).isEqualTo(record.duration);
-        assertThat(recordFromSharedPreferences).isEqualTo(record);
-    }
-
-    @Test
-    public void testCircularHistoryQueue() throws InterruptedException {
-        startAndWaitForComplete();
-        final long startChannelId = 100000000;
-        long time = System.currentTimeMillis();
-        long duration = TimeUnit.MINUTES.toMillis(10);
-
-        int size = MAX_HISTORY_SIZE * 2;
-        for (int i = 0; i < size; ++i) {
-            ChannelImpl channel = new ChannelImpl.Builder().setId(startChannelId + i).build();
-            mWatchedHistoryManager.logChannelViewStop(channel, time + duration * i, duration);
-        }
-        for (int i = 0; i < MAX_HISTORY_SIZE; ++i) {
-            WatchedRecord record = mWatchedHistoryManager.getRecord(i);
-            WatchedRecord recordFromSharedPreferences =
-                    mWatchedHistoryManager.getRecordFromSharedPreferences(i);
-            assertThat(recordFromSharedPreferences).isEqualTo(record);
-            assertThat(startChannelId + size - 1 - i).isEqualTo(record.channelId);
-        }
-        // Since the WatchedHistory is a circular queue, the value for 0 and maxHistorySize
-        // are same.
-        assertThat(mWatchedHistoryManager.getRecordFromSharedPreferences(MAX_HISTORY_SIZE))
-                .isEqualTo(mWatchedHistoryManager.getRecordFromSharedPreferences(0));
-    }
-
-    @Test
-    public void testWatchedRecordEquals() {
-        assertThat(new WatchedRecord(1, 2, 3).equals(new WatchedRecord(1, 2, 3))).isTrue();
-        assertThat(new WatchedRecord(1, 2, 3).equals(new WatchedRecord(1, 2, 4))).isFalse();
-        assertThat(new WatchedRecord(1, 2, 3).equals(new WatchedRecord(1, 4, 3))).isFalse();
-        assertThat(new WatchedRecord(1, 2, 3).equals(new WatchedRecord(4, 2, 3))).isFalse();
-    }
-
-    @Test
-    public void testEncodeDecodeWatchedRecord() {
-        long fakeId = 100000000;
-        long time = System.currentTimeMillis();
-        long duration = TimeUnit.MINUTES.toMillis(10);
-        WatchedRecord record = new WatchedRecord(fakeId, time, duration);
-        WatchedRecord sameRecord =
-                mWatchedHistoryManager.decode(mWatchedHistoryManager.encode(record));
-        assertThat(sameRecord).isEqualTo(record);
-    }
-
-    private static final class TestWatchedHistoryManagerListener
-            implements WatchedHistoryManager.Listener {
-        boolean mLoadFinished;
-
-        @Override
-        public void onLoadFinished() {
-            mLoadFinished = true;
-        }
-
-        @Override
-        public void onNewRecordAdded(WatchedRecord watchedRecord) {}
-    }
-}
diff --git a/tests/unit/src/com/android/tv/dvr/recorder/DvrRecordingServiceTest.java b/tests/unit/src/com/android/tv/dvr/recorder/DvrRecordingServiceTest.java
index d510da3..546f074 100644
--- a/tests/unit/src/com/android/tv/dvr/recorder/DvrRecordingServiceTest.java
+++ b/tests/unit/src/com/android/tv/dvr/recorder/DvrRecordingServiceTest.java
@@ -20,9 +20,9 @@
 
 import android.content.Intent;
 import android.os.Build;
-import android.support.test.filters.SdkSuppress;
-import android.support.test.filters.SmallTest;
 import android.test.ServiceTestCase;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
 import com.android.tv.common.feature.CommonFeatures;
 import com.android.tv.common.feature.TestableFeature;
 import org.mockito.MockitoAnnotations;
diff --git a/tests/unit/src/com/android/tv/FeaturesTest.java b/tests/unit/src/com/android/tv/features/FeaturesTest.java
similarity index 90%
rename from tests/unit/src/com/android/tv/FeaturesTest.java
rename to tests/unit/src/com/android/tv/features/FeaturesTest.java
index e19f4b7..e35758c 100644
--- a/tests/unit/src/com/android/tv/FeaturesTest.java
+++ b/tests/unit/src/com/android/tv/features/FeaturesTest.java
@@ -14,12 +14,12 @@
  * limitations under the License
  */
 
-package com.android.tv;
+package com.android.tv.features;
 
 import static com.google.common.truth.Truth.assertThat;
 
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
diff --git a/tests/unit/src/com/android/tv/menu/MenuTest.java b/tests/unit/src/com/android/tv/menu/MenuTest.java
index 028a185..e384c39 100644
--- a/tests/unit/src/com/android/tv/menu/MenuTest.java
+++ b/tests/unit/src/com/android/tv/menu/MenuTest.java
@@ -15,11 +15,11 @@
  */
 package com.android.tv.menu;
 
-import static android.support.test.InstrumentationRegistry.getTargetContext;
+import static androidx.test.InstrumentationRegistry.getTargetContext;
 import static com.google.common.truth.Truth.assertWithMessage;
 
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
 import com.android.tv.menu.Menu.OnMenuVisibilityChangeListener;
 import org.junit.Before;
 import org.junit.Ignore;
diff --git a/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java b/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java
index 0f815a7..5ecbdf0 100644
--- a/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java
+++ b/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java
@@ -15,15 +15,15 @@
  */
 package com.android.tv.menu;
 
-import static android.support.test.InstrumentationRegistry.getInstrumentation;
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static org.junit.Assert.fail;
 
 import android.media.tv.TvTrackInfo;
 import android.os.SystemClock;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
 import android.text.TextUtils;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
 import com.android.tv.testing.activities.BaseMainActivityTestCase;
 import com.android.tv.testing.constants.Constants;
 import com.android.tv.testing.testinput.ChannelState;
diff --git a/tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java b/tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java
index e63bdc3..c217222 100644
--- a/tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java
+++ b/tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java
@@ -16,11 +16,11 @@
 
 package com.android.tv.recommendation;
 
-import static android.support.test.InstrumentationRegistry.getContext;
+import static androidx.test.InstrumentationRegistry.getContext;
 import static com.google.common.truth.Truth.assertThat;
 
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
 import com.android.tv.testing.utils.Utils;
 import java.util.Random;
 import java.util.concurrent.TimeUnit;
diff --git a/tests/unit/src/com/android/tv/recommendation/EvaluatorTestCase.java b/tests/unit/src/com/android/tv/recommendation/EvaluatorTestCase.java
index f62a5e0..9696d8b 100644
--- a/tests/unit/src/com/android/tv/recommendation/EvaluatorTestCase.java
+++ b/tests/unit/src/com/android/tv/recommendation/EvaluatorTestCase.java
@@ -16,7 +16,7 @@
 
 package com.android.tv.recommendation;
 
-import static android.support.test.InstrumentationRegistry.getContext;
+import static androidx.test.InstrumentationRegistry.getContext;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
diff --git a/tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java b/tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java
index e14320f..773d335 100644
--- a/tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java
+++ b/tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java
@@ -18,8 +18,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 import org.junit.Test;
diff --git a/tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java b/tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java
index f8d6b22..15a3726 100644
--- a/tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java
+++ b/tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java
@@ -18,8 +18,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
diff --git a/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java b/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java
index 812a3eb..01208d2 100644
--- a/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java
+++ b/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java
@@ -16,12 +16,12 @@
 
 package com.android.tv.recommendation;
 
-import static android.support.test.InstrumentationRegistry.getContext;
+import static androidx.test.InstrumentationRegistry.getContext;
 import static com.google.common.truth.Truth.assertThat;
 
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
 import android.test.MoreAsserts;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
 import com.android.tv.data.api.Channel;
 import com.android.tv.recommendation.RecommendationUtils.ChannelRecordSortedMapHelper;
 import com.android.tv.testing.utils.Utils;
diff --git a/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java b/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java
index 39e6e9c..d914905 100644
--- a/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java
+++ b/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java
@@ -19,8 +19,8 @@
 import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertEquals;
 
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
 import com.android.tv.data.Program;
 import com.android.tv.recommendation.RoutineWatchEvaluator.ProgramTime;
 import java.util.Calendar;
diff --git a/tests/unit/src/com/android/tv/util/MockTvSingletons.java b/tests/unit/src/com/android/tv/util/MockTvSingletons.java
index 6de1eb3..fd4b43c 100644
--- a/tests/unit/src/com/android/tv/util/MockTvSingletons.java
+++ b/tests/unit/src/com/android/tv/util/MockTvSingletons.java
@@ -17,16 +17,19 @@
 package com.android.tv.util;
 
 import android.content.Context;
-import android.content.Intent;
 import com.android.tv.InputSessionManager;
 import com.android.tv.MainActivityWrapper;
 import com.android.tv.TvApplication;
 import com.android.tv.TvSingletons;
 import com.android.tv.analytics.Analytics;
 import com.android.tv.analytics.Tracker;
-import com.android.tv.common.config.api.RemoteConfig;
 import com.android.tv.common.experiments.ExperimentLoader;
+import com.android.tv.common.flags.impl.DefaultBackendKnobsFlags;
+import com.android.tv.common.flags.impl.DefaultCloudEpgFlags;
+import com.android.tv.common.flags.impl.DefaultConcurrentDvrPlaybackFlags;
+import com.android.tv.common.flags.impl.DefaultUiFlags;
 import com.android.tv.common.recording.RecordingStorageStatusManager;
+import com.android.tv.common.singletons.HasSingletons;
 import com.android.tv.common.util.Clock;
 import com.android.tv.data.ChannelDataManager;
 import com.android.tv.data.PreviewDataManager;
@@ -40,15 +43,21 @@
 import com.android.tv.dvr.recorder.RecordingScheduler;
 import com.android.tv.perf.PerformanceMonitor;
 import com.android.tv.testing.FakeClock;
-import com.android.tv.tuner.TunerInputController;
+import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
+import com.google.common.base.Optional;
 import java.util.concurrent.Executor;
 import javax.inject.Provider;
 
 /** Mock {@link TvSingletons} class. */
-public class MockTvSingletons implements TvSingletons {
+public class MockTvSingletons implements TvSingletons, HasSingletons<TvSingletons> {
     public final FakeClock fakeClock = FakeClock.createWithCurrentTime();
 
     private final TvApplication mApp;
+    private final DefaultBackendKnobsFlags mBackendFlags = new DefaultBackendKnobsFlags();
+    private final DefaultCloudEpgFlags mCloudEpgFlags = new DefaultCloudEpgFlags();
+    private final DefaultUiFlags mUiFlags = new DefaultUiFlags();
+    private final DefaultConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags =
+            new DefaultConcurrentDvrPlaybackFlags();
     private PerformanceMonitor mPerformanceMonitor;
 
     public MockTvSingletons(Context context) {
@@ -154,8 +163,8 @@
     }
 
     @Override
-    public TunerInputController getTunerInputController() {
-        return mApp.getTunerInputController();
+    public Optional<BuiltInTunerManager> getBuiltInTunerManager() {
+        return mApp.getBuiltInTunerManager();
     }
 
     @Override
@@ -174,16 +183,6 @@
     }
 
     @Override
-    public RemoteConfig getRemoteConfig() {
-        return mApp.getRemoteConfig();
-    }
-
-    @Override
-    public Intent getTunerSetupIntent(Context context) {
-        return mApp.getTunerSetupIntent(context);
-    }
-
-    @Override
     public boolean isRunningInMainProcess() {
         return mApp.isRunningInMainProcess();
     }
@@ -198,12 +197,37 @@
     }
 
     @Override
-    public String getEmbeddedTunerInputId() {
-        return "com.android.tv/.tuner.tvinput.LiveTvTunerTvInputService";
+    public DefaultCloudEpgFlags getCloudEpgFlags() {
+        return mCloudEpgFlags;
+    }
+
+    @Override
+    public DefaultUiFlags getUiFlags() {
+        return mUiFlags;
     }
 
     @Override
     public Executor getDbExecutor() {
         return mApp.getDbExecutor();
     }
+
+    @Override
+    public DefaultBackendKnobsFlags getBackendKnobs() {
+        return mBackendFlags;
+    }
+
+    @Override
+    public BuildType getBuildType() {
+        return BuildType.ENG;
+    }
+
+    @Override
+    public DefaultConcurrentDvrPlaybackFlags getConcurrentDvrPlaybackFlags() {
+        return mConcurrentDvrPlaybackFlags;
+    }
+
+    @Override
+    public TvSingletons singletons() {
+        return this;
+    }
 }
diff --git a/tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java b/tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java
index 6dfed64..7e35d76 100644
--- a/tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java
+++ b/tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java
@@ -16,12 +16,12 @@
 
 package com.android.tv.util;
 
-import static android.support.test.InstrumentationRegistry.getContext;
+import static androidx.test.InstrumentationRegistry.getContext;
 
 import android.content.pm.ResolveInfo;
 import android.media.tv.TvInputInfo;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
 import com.android.tv.testing.ComparatorTester;
 import com.android.tv.testing.utils.TestUtils;
 import java.util.ArrayList;
diff --git a/tests/unit/src/com/android/tv/util/TvTrackInfoUtilsTest.java b/tests/unit/src/com/android/tv/util/TvTrackInfoUtilsTest.java
deleted file mode 100644
index d84a90d..0000000
--- a/tests/unit/src/com/android/tv/util/TvTrackInfoUtilsTest.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tv.util;
-
-import static com.android.tv.util.TvTrackInfoUtils.getBestTrackInfo;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import android.media.tv.TvTrackInfo;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-import com.android.tv.testing.ComparatorTester;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/** Tests for {@link com.android.tv.util.TvTrackInfoUtils}. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class TvTrackInfoUtilsTest {
-    private static final String UN_MATCHED_ID = "no matching ID";
-
-    private static final TvTrackInfo INFO_1_EN_1 = create("1", "en", 1);
-
-    private static final TvTrackInfo INFO_2_EN_5 = create("2", "en", 5);
-
-    private static final TvTrackInfo INFO_3_FR_8 = create("3", "fr", 8);
-
-    private static final TvTrackInfo INFO_4_NULL_2 = create("4", null, 2);
-
-    private static final TvTrackInfo INFO_5_NULL_6 = create("5", null, 6);
-
-    private static TvTrackInfo create(String id, String fr, int audioChannelCount) {
-        return new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, id)
-                .setLanguage(fr)
-                .setAudioChannelCount(audioChannelCount)
-                .build();
-    }
-
-    private static final List<TvTrackInfo> ALL =
-            Arrays.asList(INFO_1_EN_1, INFO_2_EN_5, INFO_3_FR_8, INFO_4_NULL_2, INFO_5_NULL_6);
-    private static final List<TvTrackInfo> NULL_LANGUAGE_TRACKS =
-            Arrays.asList(INFO_4_NULL_2, INFO_5_NULL_6);
-
-    @Test
-    public void testGetBestTrackInfo_empty() {
-        TvTrackInfo result = getBestTrackInfo(Collections.emptyList(), UN_MATCHED_ID, "en", 1);
-    assertWithMessage("best track ").that(result).isEqualTo(null);
-    }
-
-    @Test
-    public void testGetBestTrackInfo_exactMatch() {
-        TvTrackInfo result = getBestTrackInfo(ALL, "1", "en", 1);
-    assertWithMessage("best track ").that(result).isEqualTo(INFO_1_EN_1);
-    }
-
-    @Test
-    public void testGetBestTrackInfo_langAndChannelCountMatch() {
-        TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, "en", 5);
-    assertWithMessage("best track ").that(result).isEqualTo(INFO_2_EN_5);
-    }
-
-    @Test
-    public void testGetBestTrackInfo_languageOnlyMatch() {
-        TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, "fr", 1);
-    assertWithMessage("best track ").that(result).isEqualTo(INFO_3_FR_8);
-    }
-
-    @Test
-    public void testGetBestTrackInfo_channelCountOnlyMatchWithNullLanguage() {
-        TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, null, 8);
-    assertWithMessage("best track ").that(result).isEqualTo(INFO_3_FR_8);
-    }
-
-    @Test
-    public void testGetBestTrackInfo_noMatches() {
-        TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, "kr", 1);
-    assertWithMessage("best track ").that(result).isEqualTo(INFO_1_EN_1);
-    }
-
-    @Test
-    public void testGetBestTrackInfo_noMatchesWithNullLanguage() {
-        TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, null, 0);
-    assertWithMessage("best track ").that(result).isEqualTo(INFO_1_EN_1);
-    }
-
-    @Test
-    public void testGetBestTrackInfo_channelCountAndIdMatch() {
-        TvTrackInfo result = getBestTrackInfo(NULL_LANGUAGE_TRACKS, "5", null, 6);
-    assertWithMessage("best track ").that(result).isEqualTo(INFO_5_NULL_6);
-    }
-
-    @Test
-    public void testComparator() {
-        Comparator<TvTrackInfo> comparator = TvTrackInfoUtils.createComparator("1", "en", 1);
-        ComparatorTester.withoutEqualsTest(comparator)
-                // lang not match
-                .addComparableGroup(
-                        create("1", "kr", 1),
-                        create("2", "kr", 2),
-                        create("1", "ja", 1),
-                        create("1", "ch", 1))
-                // lang match not count match
-                .addComparableGroup(
-                        create("2", "en", 2), create("3", "en", 3), create("1", "en", 2))
-                // lang and count match
-                .addComparableGroup(create("2", "en", 1), create("3", "en", 1))
-                // all match
-                .addComparableGroup(create("1", "en", 1), create("1", "en", 1))
-                .test();
-    }
-}
diff --git a/tests/unit/src/com/android/tv/util/images/ImageCacheTest.java b/tests/unit/src/com/android/tv/util/images/ImageCacheTest.java
index b7715c4..4172213 100644
--- a/tests/unit/src/com/android/tv/util/images/ImageCacheTest.java
+++ b/tests/unit/src/com/android/tv/util/images/ImageCacheTest.java
@@ -20,8 +20,8 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import android.graphics.Bitmap;
-import android.support.test.filters.MediumTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
 import com.android.tv.util.images.BitmapUtils.ScaledBitmapInfo;
 import org.junit.Before;
 import org.junit.Test;
diff --git a/tests/unit/src/com/android/tv/util/images/ScaledBitmapInfoTest.java b/tests/unit/src/com/android/tv/util/images/ScaledBitmapInfoTest.java
index 005775b..1bb650f 100644
--- a/tests/unit/src/com/android/tv/util/images/ScaledBitmapInfoTest.java
+++ b/tests/unit/src/com/android/tv/util/images/ScaledBitmapInfoTest.java
@@ -18,8 +18,8 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import android.graphics.Bitmap;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
 import com.android.tv.util.images.BitmapUtils.ScaledBitmapInfo;
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/tuner/Android.bp b/tuner/Android.bp
new file mode 100644
index 0000000..215a1e5
--- /dev/null
+++ b/tuner/Android.bp
@@ -0,0 +1,48 @@
+//
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+android_library {
+    name: "live-tv-tuner",
+    srcs: ["src/**/*.java"],
+    sdk_version: "system_current",
+    resource_dirs: ["res"],
+    libs: [
+        "tv-auto-value-jar",
+        "tv-auto-factory-jar",
+        "android-support-annotations",
+        "tv-error-prone-annotations-jar",
+        "tv-guava-android-jar",
+        "tv-javax-annotations-jar",
+        "jsr330",
+        "tv-lib-dagger",
+        "tv-lib-exoplayer",
+        "tv-lib-exoplayer-v2-core",
+        "live-tv-tuner-proto",
+        "android-support-compat",
+        "android-support-core-ui",
+        "android-support-v7-palette",
+        "android-support-v7-recyclerview",
+        "android-support-v17-leanback",
+        "androidx.tvprovider_tvprovider",
+        "tv-lib-dagger-android",
+        "tv-common",
+    ],
+    plugins: [
+        "tv-auto-value",
+        "tv-auto-factory",
+    ],
+    min_sdk_version: "23",
+}
diff --git a/tuner/Android.mk b/tuner/Android.mk
deleted file mode 100644
index aedda3c..0000000
--- a/tuner/Android.mk
+++ /dev/null
@@ -1,44 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-# Include all java and proto files.
-LOCAL_SRC_FILES := \
-    $(call all-java-files-under, src) \
-    $(call all-proto-files-under, proto)
-
-
-LOCAL_MODULE := live-tv-tuner
-LOCAL_MODULE_CLASS := STATIC_JAVA_LIBRARIES
-LOCAL_MODULE_TAGS := optional
-LOCAL_SDK_VERSION := system_current
-
-LOCAL_USE_AAPT2 := true
-
-LOCAL_PROTOC_OPTIMIZE_TYPE := nano
-LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)/proto/
-LOCAL_PROTO_JAVA_OUTPUT_PARAMS := enum_style=java
-
-LOCAL_RESOURCE_DIR := \
-    $(LOCAL_PATH)/res \
-
-LOCAL_JAVA_LIBRARIES := \
-    android-support-annotations \
-    lib-exoplayer \
-    lib-exoplayer-v2-core \
-
-LOCAL_SHARED_ANDROID_LIBRARIES := \
-    android-support-compat \
-    android-support-core-ui \
-    android-support-tv-provider \
-    android-support-v7-palette \
-    android-support-v7-recyclerview \
-    android-support-v17-leanback \
-    android-support-tv-provider \
-    tv-common \
-
-LOCAL_MIN_SDK_VERSION := 23
-
-include $(LOCAL_PATH)/buildconfig.mk
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
-
diff --git a/tuner/AndroidManifest.xml b/tuner/AndroidManifest.xml
index af80f69..fd21771 100644
--- a/tuner/AndroidManifest.xml
+++ b/tuner/AndroidManifest.xml
@@ -15,8 +15,10 @@
   ~ limitations under the License
   -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
     package="com.android.tv.tuner"
     android:versionCode="1">
-  <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="21"/>
-  <application />
+  <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
+  <application tools:replace="android:appComponentFactory"
+      android:appComponentFactory="android.support.v4.app.CoreComponentFactory" />
 </manifest>
diff --git a/tuner/BuildConfig.java.in b/tuner/BuildConfig.java.in
deleted file mode 100644
index 85967fa..0000000
--- a/tuner/BuildConfig.java.in
+++ /dev/null
@@ -1,8 +0,0 @@
-/* This file is auto generated. Do not modify. */
-package com.android.tv.tuner;
-
-public final class BuildConfig {
-    public static final boolean DEBUG = %DEBUG%;
-    public static final boolean ENG = %ENG%;
-    private BuildConfig() {}
-}
\ No newline at end of file
diff --git a/tuner/SampleDvbTuner/AndroidManifest.xml b/tuner/SampleDvbTuner/AndroidManifest.xml
index 740989a..5ad927e 100755
--- a/tuner/SampleDvbTuner/AndroidManifest.xml
+++ b/tuner/SampleDvbTuner/AndroidManifest.xml
@@ -19,7 +19,7 @@
 
     <uses-sdk
         android:minSdkVersion="23"
-        android:targetSdkVersion="26" />
+        android:targetSdkVersion="27" />
 
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@@ -50,12 +50,10 @@
 
     <application
         android:name="com.android.tv.tuner.sample.dvb.app.SampleDvbTuner"
+        android:appComponentFactory="android.support.v4.app.CoreComponentFactory"
         android:icon="@mipmap/ic_launcher"
         android:label="@string/sample_dvb_tuner_app_name" >
-        <activity
-            android:name="com.google.android.gms.common.api.GoogleApiActivity"
-            android:exported="false"
-            android:theme="@android:style/Theme.Translucent.NoTitleBar" />
+
 
         <activity
             android:name="com.android.tv.tuner.sample.dvb.setup.SampleDvbTunerSetupActivity"
@@ -73,7 +71,7 @@
             android:name="com.android.tv.tuner.sample.dvb.tvinput.SampleDvbTunerTvInputService"
             android:label="@string/sample_dvb_tuner_app_name"
             android:permission="android.permission.BIND_TV_INPUT"
-            android:process="com.google.android.tv.tuner.sample.dvb.tvinput" >
+            android:process="com.android.tv.tuner.sample.dvb.tvinput" >
             <intent-filter>
                 <action android:name="android.media.tv.TvInputService" />
             </intent-filter>
diff --git a/tuner/SampleDvbTuner/ResourceManifest.xml b/tuner/SampleDvbTuner/ResourceManifest.xml
deleted file mode 100644
index e13f958..0000000
--- a/tuner/SampleDvbTuner/ResourceManifest.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2018 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.tv.tuner.sample.dvb" xmlns:tools="http://schemas.android.com/tools">
-
-    <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="23"/>
-
-   <!-- Only used for resources-->
-</manifest>
diff --git a/tuner/SampleDvbTuner/build.gradle b/tuner/SampleDvbTuner/build.gradle
new file mode 100644
index 0000000..657a425
--- /dev/null
+++ b/tuner/SampleDvbTuner/build.gradle
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ * Experimental gradle configuration.  This file may not be up to date.
+ */
+
+buildscript {
+    repositories {
+        mavenCentral()
+        google()
+        jcenter()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.1.4'
+        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.5'
+    }
+}
+apply plugin: 'com.android.application'
+apply plugin: 'com.google.protobuf'
+android {
+    compileSdkVersion 26
+    buildToolsVersion '28.0.2'
+
+    dexOptions {
+        preDexLibraries = false
+        additionalParameters=['--core-library']
+        javaMaxHeapSize "6g"
+    }
+    android {
+        defaultConfig {
+            resConfigs "en"
+        }
+    }
+    defaultConfig {
+        minSdkVersion 23
+        targetSdkVersion 26
+        versionCode 1
+        versionName "1.0"
+    }
+    buildTypes {
+        debug {
+            minifyEnabled false
+        }
+    }
+    compileOptions() {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+    sourceSets {
+        main {
+            res.srcDirs = ['res']
+            java.srcDirs = ['src', '../../partner_support/src']
+            manifest.srcFile 'AndroidManifest.xml'
+        }
+    }
+}
+
+repositories {
+    mavenCentral()
+    jcenter()
+    google()
+}
+
+final String SUPPORT_LIBS_VERSION = '26.1.0'
+dependencies {
+    implementation 'com.google.android.exoplayer:exoplayer-core:2.9.0'
+    implementation 'com.google.android.exoplayer:exoplayer:r1.5.16'
+    implementation "com.android.support:palette-v7:${SUPPORT_LIBS_VERSION}"
+    implementation "com.android.support:leanback-v17:${SUPPORT_LIBS_VERSION}"
+    implementation "com.android.support:support-tv-provider:${SUPPORT_LIBS_VERSION}"
+    /*Not building with  latest one (1.6.2)*/
+    annotationProcessor 'com.google.auto.value:auto-value:1.5.4'
+    implementation 'com.google.auto.value:auto-value:1.5.4'
+    implementation project(':common')
+    implementation project(':tuner')
+}
diff --git a/tuner/SampleDvbTuner/res/mipmap-hdpi/ic_launcher.png b/tuner/SampleDvbTuner/res/mipmap-hdpi/ic_launcher.png
index b5c5170..9735fec 100644
--- a/tuner/SampleDvbTuner/res/mipmap-hdpi/ic_launcher.png
+++ b/tuner/SampleDvbTuner/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/tuner/SampleDvbTuner/res/mipmap-mdpi/ic_launcher.png b/tuner/SampleDvbTuner/res/mipmap-mdpi/ic_launcher.png
index 6629721..3bb9480 100644
--- a/tuner/SampleDvbTuner/res/mipmap-mdpi/ic_launcher.png
+++ b/tuner/SampleDvbTuner/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/tuner/SampleDvbTuner/res/mipmap-xhdpi/ic_launcher.png b/tuner/SampleDvbTuner/res/mipmap-xhdpi/ic_launcher.png
index f259d1c..c1c9c73 100644
--- a/tuner/SampleDvbTuner/res/mipmap-xhdpi/ic_launcher.png
+++ b/tuner/SampleDvbTuner/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/tuner/SampleDvbTuner/res/mipmap-xxhdpi/ic_launcher.png b/tuner/SampleDvbTuner/res/mipmap-xxhdpi/ic_launcher.png
index 421cd08..0556c2c 100644
--- a/tuner/SampleDvbTuner/res/mipmap-xxhdpi/ic_launcher.png
+++ b/tuner/SampleDvbTuner/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/tuner/SampleDvbTuner/res/mipmap-xxxhdpi/ic_launcher.png b/tuner/SampleDvbTuner/res/mipmap-xxxhdpi/ic_launcher.png
index 91be322..652fc45 100644
--- a/tuner/SampleDvbTuner/res/mipmap-xxxhdpi/ic_launcher.png
+++ b/tuner/SampleDvbTuner/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/src/com/android/tv/util/Filter.java b/tuner/SampleDvbTuner/settings.gradle
similarity index 67%
copy from src/com/android/tv/util/Filter.java
copy to tuner/SampleDvbTuner/settings.gradle
index 3e24a49..13cb90f 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/tuner/SampleDvbTuner/settings.gradle
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,10 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.tv.util;
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
-}
+/*
+ * Experimental gradle configuration.  This file may not be up to date.
+ */
+
+include ':common'
+include ':tuner'
+project(":common").projectDir = file("../../common")
+project(":tuner").projectDir = file(".././")
diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/AndroidManifest.xml b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/AndroidManifest.xml
index adb8e30..dc04228 100644
--- a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/AndroidManifest.xml
+++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/AndroidManifest.xml
@@ -36,6 +36,10 @@
     <uses-feature android:name="android.software.leanback" android:required="true" />
     <uses-feature android:name="android.software.live_tv" android:required="true" />
     <uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
-    <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="23"/>
-    <application />
+    <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
+    <application
+        android:name=".app.SampleDvbTuner"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/sample_dvb_tuner_app_name"
+        />
 </manifest>
diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTuner.java b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTuner.java
index 15e9043..568e3c9 100644
--- a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTuner.java
+++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTuner.java
@@ -17,30 +17,39 @@
 package com.android.tv.tuner.sample.dvb.app;
 
 import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
 import android.media.tv.TvContract;
 import com.android.tv.common.BaseApplication;
-import com.android.tv.common.actions.InputSetupActionUtils;
-import com.android.tv.common.config.DefaultConfigManager;
-import com.android.tv.common.config.api.RemoteConfig;
-import com.android.tv.common.util.CommonUtils;
+import com.android.tv.common.singletons.HasSingletons;
+import com.android.tv.tuner.modules.TunerSingletonsModule;
+import com.android.tv.tuner.sample.dvb.singletons.SampleDvbSingletons;
 import com.android.tv.tuner.sample.dvb.tvinput.SampleDvbTunerTvInputService;
-import com.android.tv.tuner.setup.LiveTvTunerSetupActivity;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactory;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactoryImpl;
+import dagger.android.AndroidInjector;
+import com.android.tv.common.flags.CloudEpgFlags;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+import javax.inject.Inject;
 
 /** The top level application for Sample DVB Tuner. */
-public class SampleDvbTuner extends BaseApplication {
+public class SampleDvbTuner extends BaseApplication
+        implements SampleDvbSingletons, HasSingletons<SampleDvbSingletons> {
+
     private String mEmbeddedInputId;
-    private RemoteConfig mRemoteConfig;
+    @Inject CloudEpgFlags mCloudEpgFlags;
+    @Inject ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
+    @Inject TunerSessionFactoryImpl mTunerSessionFactory;
 
     @Override
-    public Intent getTunerSetupIntent(Context context) {
-        // Make an intent to launch the setup activity of TV tuner input.
-        Intent intent =
-                CommonUtils.createSetupIntent(
-                        new Intent(context, LiveTvTunerSetupActivity.class), mEmbeddedInputId);
-        intent.putExtra(InputSetupActionUtils.EXTRA_INPUT_ID, mEmbeddedInputId);
-        return intent;
+    public void onCreate() {
+        super.onCreate();
+    }
+
+    @Override
+    protected AndroidInjector<SampleDvbTuner> applicationInjector() {
+        return DaggerSampleDvbTunerComponent.builder()
+                .sampleDvbTunerModule(new SampleDvbTunerModule(this))
+                .tunerSingletonsModule(new TunerSingletonsModule(this))
+                .build();
     }
 
     @Override
@@ -54,11 +63,26 @@
     }
 
     @Override
-    public RemoteConfig getRemoteConfig() {
-        if (mRemoteConfig == null) {
-            // No need to synchronize this, it does not hurt to create two and throw one away.
-            mRemoteConfig = DefaultConfigManager.createInstance(this).getRemoteConfig();
-        }
-        return mRemoteConfig;
+    public CloudEpgFlags getCloudEpgFlags() {
+        return mCloudEpgFlags;
+    }
+
+    @Override
+    public BuildType getBuildType() {
+        return BuildType.ENG;
+    }
+
+    @Override
+    public ConcurrentDvrPlaybackFlags getConcurrentDvrPlaybackFlags() {
+        return mConcurrentDvrPlaybackFlags;
+    }
+
+    @Override
+    public SampleDvbSingletons singletons() {
+        return this;
+    }
+
+    public TunerSessionFactory getTunerSessionFactory() {
+        return mTunerSessionFactory;
     }
 }
diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTunerComponent.java b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTunerComponent.java
new file mode 100644
index 0000000..e6c80ea
--- /dev/null
+++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTunerComponent.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.sample.dvb.app;
+
+import dagger.Component;
+import dagger.android.AndroidInjectionModule;
+import dagger.android.AndroidInjector;
+import javax.inject.Singleton;
+
+/** Dagger component for {@link SampleDvbTuner}. */
+@Singleton
+@Component(modules = {AndroidInjectionModule.class, SampleDvbTunerModule.class})
+public interface SampleDvbTunerComponent extends AndroidInjector<SampleDvbTuner> {}
diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTunerModule.java b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTunerModule.java
new file mode 100644
index 0000000..4da3ca9
--- /dev/null
+++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTunerModule.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.sample.dvb.app;
+
+import com.android.tv.common.flags.impl.DefaultFlagsModule;
+import com.android.tv.tuner.api.TunerFactory;
+import com.android.tv.tuner.builtin.BuiltInTunerHalFactory;
+import com.android.tv.tuner.modules.TunerModule;
+import com.android.tv.tuner.sample.dvb.setup.SampleDvbTunerSetupActivity;
+import com.android.tv.tuner.sample.dvb.tvinput.SampleDvbTunerTvInputService;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactory;
+import dagger.Module;
+import dagger.Provides;
+
+/** Dagger module for {@link SampleDvbTuner}. */
+@Module(
+        includes = {
+            DefaultFlagsModule.class,
+            SampleDvbTunerTvInputService.Module.class,
+            SampleDvbTunerSetupActivity.Module.class,
+            TunerModule.class,
+        })
+class SampleDvbTunerModule {
+    private final SampleDvbTuner mSampleDvbTuner;
+
+    SampleDvbTunerModule(SampleDvbTuner sampleDvbTuner) {
+        mSampleDvbTuner = sampleDvbTuner;
+    }
+
+    @Provides
+    public TunerSessionFactory providesTunerSessionFactory() {
+        return mSampleDvbTuner.getTunerSessionFactory();
+    }
+
+    @Provides
+    TunerFactory providesTunerFactory() {
+        return BuiltInTunerHalFactory.INSTANCE;
+    }
+}
diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/setup/SampleDvbTunerSetupActivity.java b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/setup/SampleDvbTunerSetupActivity.java
index 54b3a9e..f9ef29c 100644
--- a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/setup/SampleDvbTunerSetupActivity.java
+++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/setup/SampleDvbTunerSetupActivity.java
@@ -20,6 +20,7 @@
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.media.tv.TvInputInfo;
 import android.os.AsyncTask;
 import android.os.Bundle;
@@ -29,26 +30,28 @@
 import android.util.Log;
 import android.util.Pair;
 import android.view.KeyEvent;
-import com.android.tv.common.BaseApplication;
 import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.singletons.HasSingletons;
 import com.android.tv.common.ui.setup.SetupFragment;
 import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
 import com.android.tv.common.util.PostalCodeUtils;
-import com.android.tv.tuner.TunerHal;
 import com.android.tv.tuner.sample.dvb.R;
 import com.android.tv.tuner.setup.BaseTunerSetupActivity;
 import com.android.tv.tuner.setup.ConnectionTypeFragment;
 import com.android.tv.tuner.setup.LineupFragment;
+import com.android.tv.tuner.setup.LocationFragment;
 import com.android.tv.tuner.setup.PostalCodeFragment;
 import com.android.tv.tuner.setup.ScanFragment;
 import com.android.tv.tuner.setup.ScanResultFragment;
 import com.android.tv.tuner.setup.WelcomeFragment;
+import com.android.tv.tuner.singletons.TunerSingletons;
 import com.google.android.tv.partner.support.EpgContract;
 import com.google.android.tv.partner.support.EpgInput;
 import com.google.android.tv.partner.support.EpgInputs;
 import com.google.android.tv.partner.support.Lineup;
 import com.google.android.tv.partner.support.Lineups;
 import com.google.android.tv.partner.support.TunerSetupUtils;
+import dagger.android.ContributesAndroidInjector;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -72,23 +75,19 @@
     private EpgInput epgInput;
     private String postalCode;
     private final Handler handler = new Handler();
-    private final Runnable cancelFetchLineupTaskRunnable =
-            new Runnable() {
-                @Override
-                public void run() {
-                    cancelFetchLineup();
-                }
-            };
+    private final Runnable cancelFetchLineupTaskRunnable = this::cancelFetchLineup;
     private String embeddedInputId;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
         if (DEBUG) {
             Log.d(TAG, "onCreate");
         }
-        embeddedInputId = BaseApplication.getSingletons(this).getEmbeddedTunerInputId();
+        embeddedInputId =
+                HasSingletons.get(TunerSingletons.class, getApplicationContext())
+                        .getEmbeddedTunerInputId();
         new QueryEpgInputTask(embeddedInputId).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
-        super.onCreate(savedInstanceState);
     }
 
     @Override
@@ -96,7 +95,7 @@
         new AsyncTask<Void, Void, Integer>() {
             @Override
             protected Integer doInBackground(Void... arg0) {
-                return TunerHal.getTunerTypeAndCount(SampleDvbTunerSetupActivity.this).first;
+                return mTunerFactory.getTunerTypeAndCount(SampleDvbTunerSetupActivity.this).first;
             }
 
             @Override
@@ -125,10 +124,16 @@
                         break;
                     default:
                         String postalCode = PostalCodeUtils.getLastPostalCode(this);
-                        if (mNeedToShowPostalCodeFragment
-                                || (CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(
+                        boolean needLocation =
+                                CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(
                                                 getApplicationContext())
-                                        && TextUtils.isEmpty(postalCode))) {
+                                        && TextUtils.isEmpty(postalCode);
+                        if (needLocation
+                                && checkSelfPermission(
+                                                android.Manifest.permission.ACCESS_COARSE_LOCATION)
+                                        != PackageManager.PERMISSION_GRANTED) {
+                            showLocationFragment();
+                        } else if (mNeedToShowPostalCodeFragment || needLocation) {
                             // We cannot get postal code automatically. Postal code input fragment
                             // should always be shown even if users have input some valid postal
                             // code in this activity before.
@@ -144,6 +149,26 @@
                         break;
                 }
                 return true;
+            case LocationFragment.ACTION_CATEGORY:
+                switch (actionId) {
+                    case LocationFragment.ACTION_ALLOW_PERMISSION:
+                        String postalCode =
+                                params == null
+                                        ? null
+                                        : params.getString(LocationFragment.KEY_POSTAL_CODE);
+                        if (postalCode == null) {
+                            showPostalCodeFragment();
+                        } else {
+                            this.postalCode = postalCode;
+                            restartFetchLineupTask();
+                            showConnectionTypeFragment();
+                        }
+                        break;
+                    default:
+                        cancelFetchLineup();
+                        showConnectionTypeFragment();
+                }
+                return true;
             case PostalCodeFragment.ACTION_CATEGORY:
                 lineups = null;
                 selectedLineup = null;
@@ -220,8 +245,7 @@
             case ScanResultFragment.ACTION_CATEGORY:
                 switch (actionId) {
                     case SetupMultiPaneFragment.ACTION_DONE:
-                        new SampleDvbTunerSetupActivity.InsertOrModifyEpgInputTask(
-                                        selectedLineup, embeddedInputId)
+                        new InsertOrModifyEpgInputTask(selectedLineup, embeddedInputId)
                                 .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
                         break;
                     default:
@@ -340,7 +364,9 @@
 
     private void restartFetchLineupTask() {
         if (!CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(getApplicationContext())
-                || TextUtils.isEmpty(postalCode)) {
+                || TextUtils.isEmpty(postalCode)
+                || checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION)
+                        != PackageManager.PERMISSION_GRANTED) {
             return;
         }
         if (fetchLineupTask != null) {
@@ -441,6 +467,16 @@
         }
     }
 
+    /**
+     * Exports {@link SampleDvbTunerSetupActivity} for Dagger codegen to create the appropriate
+     * injector.
+     */
+    @dagger.Module
+    public abstract static class Module {
+        @ContributesAndroidInjector
+        abstract SampleDvbTunerSetupActivity contributeSampleDvbTunerSetupActivityInjector();
+    }
+
     private class QueryEpgInputTask extends AsyncTask<Void, Void, EpgInput> {
         private final String inputId;
 
diff --git a/src/com/android/tv/util/Filter.java b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/singletons/SampleDvbSingletons.java
similarity index 62%
copy from src/com/android/tv/util/Filter.java
copy to tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/singletons/SampleDvbSingletons.java
index 3e24a49..22a6790 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/singletons/SampleDvbSingletons.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.tv.util;
+package com.android.tv.tuner.sample.dvb.singletons;
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
-}
+import com.android.tv.common.BaseSingletons;
+import com.android.tv.tuner.singletons.TunerSingletons;
+
+/** Singletons for SampleDvbTuner. */
+public interface SampleDvbSingletons extends BaseSingletons, TunerSingletons {}
diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/tvinput/SampleDvbTunerTvInputService.java b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/tvinput/SampleDvbTunerTvInputService.java
index ae15aff..a31faa8 100644
--- a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/tvinput/SampleDvbTunerTvInputService.java
+++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/tvinput/SampleDvbTunerTvInputService.java
@@ -16,6 +16,18 @@
 package com.android.tv.tuner.sample.dvb.tvinput;
 
 import com.android.tv.tuner.tvinput.BaseTunerTvInputService;
+import dagger.android.ContributesAndroidInjector;
 
 /** Sample DVB Tuner {@link android.media.tv.TvInputService}. */
-public class SampleDvbTunerTvInputService extends BaseTunerTvInputService {}
+public class SampleDvbTunerTvInputService extends BaseTunerTvInputService {
+
+    /**
+     * Exports {@link SampleDvbTunerTvInputService} for Dagger codegen to create the appropriate
+     * injector.
+     */
+    @dagger.Module
+    public abstract static class Module {
+        @ContributesAndroidInjector
+        abstract SampleDvbTunerTvInputService contributesSampleDvbTunerTvInputServiceInjector();
+    }
+}
diff --git a/tuner/SampleNetworkTuner/AndroidManifest.xml b/tuner/SampleNetworkTuner/AndroidManifest.xml
new file mode 100755
index 0000000..0ec9afc
--- /dev/null
+++ b/tuner/SampleNetworkTuner/AndroidManifest.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.tv.tuner.sample.network" >
+
+    <uses-sdk
+        android:minSdkVersion="23"
+        android:targetSdkVersion="27" />
+
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.READ_CONTENT_RATING_SYSTEMS" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.READ_TV_LISTINGS" />
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+    <uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" />
+    <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
+
+    <!-- Permissions/feature for USB tuner -->
+    <uses-permission android:name="android.permission.DVB_DEVICE" />
+
+    <uses-feature
+        android:name="android.hardware.usb.host"
+        android:required="false" />
+
+    <!-- Limit only for Android TV -->
+    <uses-feature
+        android:name="android.software.leanback"
+        android:required="true" />
+    <uses-feature
+        android:name="android.software.live_tv"
+        android:required="true" />
+    <uses-feature
+        android:name="android.hardware.touchscreen"
+        android:required="false" />
+
+    <application
+        android:name="com.android.tv.tuner.sample.network.app.SampleNetworkTuner"
+        android:appComponentFactory="android.support.v4.app.CoreComponentFactory"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/sample_network_tuner_app_name" >
+
+
+        <activity
+            android:name="com.android.tv.tuner.sample.network.setup.SampleNetworkTunerSetupActivity"
+            android:configChanges="keyboard|keyboardHidden"
+            android:exported="true"
+            android:label="@string/sample_network_tuner_app_name"
+            android:launchMode="singleInstance"
+            android:theme="@style/Theme.Setup.GuidedStep" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+            </intent-filter>
+        </activity>
+
+        <service
+            android:name="com.android.tv.tuner.sample.network.tvinput.SampleNetworkTunerTvInputService"
+            android:label="@string/sample_network_tuner_app_name"
+            android:permission="android.permission.BIND_TV_INPUT"
+            android:process="com.android.tv.tuner.sample.network.tvinput" >
+            <intent-filter>
+                <action android:name="android.media.tv.TvInputService" />
+            </intent-filter>
+
+            <meta-data
+                android:name="android.media.tv.input"
+                android:resource="@xml/sample_network_tvinputservice" />
+        </service>
+        <service
+            android:name="com.android.tv.tuner.tvinput.TunerStorageCleanUpService"
+            android:exported="false"
+            android:permission="android.permission.BIND_JOB_SERVICE"
+            android:process="com.android.tv.tuner" />
+    </application>
+
+</manifest>
\ No newline at end of file
diff --git a/tuner/SampleNetworkTuner/build.gradle b/tuner/SampleNetworkTuner/build.gradle
new file mode 100644
index 0000000..657a425
--- /dev/null
+++ b/tuner/SampleNetworkTuner/build.gradle
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ * Experimental gradle configuration.  This file may not be up to date.
+ */
+
+buildscript {
+    repositories {
+        mavenCentral()
+        google()
+        jcenter()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.1.4'
+        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.5'
+    }
+}
+apply plugin: 'com.android.application'
+apply plugin: 'com.google.protobuf'
+android {
+    compileSdkVersion 26
+    buildToolsVersion '28.0.2'
+
+    dexOptions {
+        preDexLibraries = false
+        additionalParameters=['--core-library']
+        javaMaxHeapSize "6g"
+    }
+    android {
+        defaultConfig {
+            resConfigs "en"
+        }
+    }
+    defaultConfig {
+        minSdkVersion 23
+        targetSdkVersion 26
+        versionCode 1
+        versionName "1.0"
+    }
+    buildTypes {
+        debug {
+            minifyEnabled false
+        }
+    }
+    compileOptions() {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+    sourceSets {
+        main {
+            res.srcDirs = ['res']
+            java.srcDirs = ['src', '../../partner_support/src']
+            manifest.srcFile 'AndroidManifest.xml'
+        }
+    }
+}
+
+repositories {
+    mavenCentral()
+    jcenter()
+    google()
+}
+
+final String SUPPORT_LIBS_VERSION = '26.1.0'
+dependencies {
+    implementation 'com.google.android.exoplayer:exoplayer-core:2.9.0'
+    implementation 'com.google.android.exoplayer:exoplayer:r1.5.16'
+    implementation "com.android.support:palette-v7:${SUPPORT_LIBS_VERSION}"
+    implementation "com.android.support:leanback-v17:${SUPPORT_LIBS_VERSION}"
+    implementation "com.android.support:support-tv-provider:${SUPPORT_LIBS_VERSION}"
+    /*Not building with  latest one (1.6.2)*/
+    annotationProcessor 'com.google.auto.value:auto-value:1.5.4'
+    implementation 'com.google.auto.value:auto-value:1.5.4'
+    implementation project(':common')
+    implementation project(':tuner')
+}
diff --git a/tuner/SampleNetworkTuner/res/mipmap-hdpi/ic_launcher.png b/tuner/SampleNetworkTuner/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..e0d8e84
--- /dev/null
+++ b/tuner/SampleNetworkTuner/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/tuner/SampleNetworkTuner/res/mipmap-mdpi/ic_launcher.png b/tuner/SampleNetworkTuner/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..5e99cdf
--- /dev/null
+++ b/tuner/SampleNetworkTuner/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/tuner/SampleNetworkTuner/res/mipmap-xhdpi/ic_launcher.png b/tuner/SampleNetworkTuner/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..2f6336f
--- /dev/null
+++ b/tuner/SampleNetworkTuner/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/tuner/SampleNetworkTuner/res/mipmap-xxhdpi/ic_launcher.png b/tuner/SampleNetworkTuner/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d14775b
--- /dev/null
+++ b/tuner/SampleNetworkTuner/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/tuner/SampleNetworkTuner/res/mipmap-xxxhdpi/ic_launcher.png b/tuner/SampleNetworkTuner/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..ab4e8df
--- /dev/null
+++ b/tuner/SampleNetworkTuner/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/tuner/SampleNetworkTuner/res/values/strings.xml b/tuner/SampleNetworkTuner/res/values/strings.xml
new file mode 100644
index 0000000..47a4c28
--- /dev/null
+++ b/tuner/SampleNetworkTuner/res/values/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <!-- Name of this application. It appears in TV app UI, as one of available TV inputs. -->
+  <string name="sample_network_tuner_app_name" translatable="false">Sample Network Tuner</string>
+</resources>
\ No newline at end of file
diff --git a/tuner/SampleNetworkTuner/res/xml/sample_network_tvinputservice.xml b/tuner/SampleNetworkTuner/res/xml/sample_network_tvinputservice.xml
new file mode 100644
index 0000000..7e6083d
--- /dev/null
+++ b/tuner/SampleNetworkTuner/res/xml/sample_network_tvinputservice.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright (C) 2015 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<!--
+/**
+ * Copyright (c) 2014, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<tv-input xmlns:android="http://schemas.android.com/apk/res/android"
+    android:setupActivity="com.android.tv.tuner.sample.network.setup.SampleNetworkTunerSetupActivity"
+    android:canRecord="true"
+    android:tunerCount="1" />
diff --git a/src/com/android/tv/util/Filter.java b/tuner/SampleNetworkTuner/settings.gradle
similarity index 67%
copy from src/com/android/tv/util/Filter.java
copy to tuner/SampleNetworkTuner/settings.gradle
index 3e24a49..13cb90f 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/tuner/SampleNetworkTuner/settings.gradle
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,10 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.tv.util;
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
-}
+/*
+ * Experimental gradle configuration.  This file may not be up to date.
+ */
+
+include ':common'
+include ':tuner'
+project(":common").projectDir = file("../../common")
+project(":tuner").projectDir = file(".././")
diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/AndroidManifest.xml b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/AndroidManifest.xml
new file mode 100644
index 0000000..dddd8a4
--- /dev/null
+++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/AndroidManifest.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.tv.tuner.sample.network" xmlns:tools="http://schemas.android.com/tools">
+
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.READ_CONTENT_RATING_SYSTEMS" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.READ_TV_LISTINGS" />
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+    <uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" />
+    <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
+
+    <!-- Permissions/feature for USB tuner -->
+    <uses-permission android:name="android.permission.DVB_DEVICE" />
+    <uses-feature android:name="android.hardware.usb.host" android:required="false" />
+
+    <!-- Limit only for Android TV -->
+    <uses-feature android:name="android.software.leanback" android:required="true" />
+    <uses-feature android:name="android.software.live_tv" android:required="true" />
+    <uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
+    <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
+    <application
+        android:name=".app.SampleNetworkTuner"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/sample_network_tuner_app_name"
+        />
+</manifest>
diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/README.md b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/README.md
new file mode 100644
index 0000000..9df86c5
--- /dev/null
+++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/README.md
@@ -0,0 +1,37 @@
+# SampleNetworkTuner
+
+SampleNetworkTuner is the reference DVB Tuner. Partners should copy these files
+to their own directory and modify as needed.
+
+## Prerequisites
+
+*   A DVB Tuner
+    *   A Nexus player with a USB Tuner attached will work.
+*   system privileged app
+    *   The DVB_DEVICE permission requires the app to be a privileged system app
+
+## First install
+
+#### Root
+
+```bash
+adb root
+adb remount
+```
+
+### modify privapp-permissions-atv.xml
+
+Edit system/etc/permissions/privapp-permissions-atv.xml
+
+```xml
+<privapp-permissions package="com.android.tv.tuner.sample.network">
+    <permission name="android.permission.DVB_DEVICE"/>
+</privapp-permissions>
+```
+
+### Push to system/priv-app
+
+```bash
+adb shell mkdir /system/priv-app/SampleNetworkTuner
+adb push <path to apk>  /system/priv-app/SampleNetworkTuner/SampleNetworkTuner.apk
+```
diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTuner.java b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTuner.java
new file mode 100644
index 0000000..eb5b2ad
--- /dev/null
+++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTuner.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.sample.network.app;
+
+import android.content.ComponentName;
+import android.media.tv.TvContract;
+import com.android.tv.common.BaseApplication;
+import com.android.tv.common.singletons.HasSingletons;
+import com.android.tv.tuner.modules.TunerSingletonsModule;
+import com.android.tv.tuner.sample.network.singletons.SampleNetworkSingletons;
+import com.android.tv.tuner.sample.network.tvinput.SampleNetworkTunerTvInputService;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactory;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactoryImpl;
+import dagger.android.AndroidInjector;
+import com.android.tv.common.flags.CloudEpgFlags;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+import javax.inject.Inject;
+
+/** The top level application for Sample DVB Tuner. */
+public class SampleNetworkTuner extends BaseApplication
+        implements SampleNetworkSingletons, HasSingletons<SampleNetworkSingletons> {
+
+    private String mEmbeddedInputId;
+    @Inject CloudEpgFlags mCloudEpgFlags;
+    @Inject ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
+    @Inject TunerSessionFactoryImpl mTunerSessionFactory;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+    }
+
+    @Override
+    protected AndroidInjector<SampleNetworkTuner> applicationInjector() {
+        return DaggerSampleNetworkTunerComponent.builder()
+                .sampleNetworkTunerModule(new SampleNetworkTunerModule(this))
+                .tunerSingletonsModule(new TunerSingletonsModule(this))
+                .build();
+    }
+
+    @Override
+    public synchronized String getEmbeddedTunerInputId() {
+        if (mEmbeddedInputId == null) {
+            mEmbeddedInputId =
+                    TvContract.buildInputId(
+                            new ComponentName(this, SampleNetworkTunerTvInputService.class));
+        }
+        return mEmbeddedInputId;
+    }
+
+    @Override
+    public CloudEpgFlags getCloudEpgFlags() {
+        return mCloudEpgFlags;
+    }
+
+    @Override
+    public BuildType getBuildType() {
+        return BuildType.ENG;
+    }
+
+    @Override
+    public ConcurrentDvrPlaybackFlags getConcurrentDvrPlaybackFlags() {
+        return mConcurrentDvrPlaybackFlags;
+    }
+
+    @Override
+    public SampleNetworkSingletons singletons() {
+        return this;
+    }
+
+    public TunerSessionFactory getTunerSessionFactory() {
+        return mTunerSessionFactory;
+    }
+}
diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTunerComponent.java b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTunerComponent.java
new file mode 100644
index 0000000..b10105b
--- /dev/null
+++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTunerComponent.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.sample.network.app;
+
+import dagger.Component;
+import dagger.android.AndroidInjectionModule;
+import dagger.android.AndroidInjector;
+import javax.inject.Singleton;
+
+/** Dagger component for {@link SampleNetworkTuner}. */
+@Singleton
+@Component(modules = {AndroidInjectionModule.class, SampleNetworkTunerModule.class})
+public interface SampleNetworkTunerComponent extends AndroidInjector<SampleNetworkTuner> {}
diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTunerModule.java b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTunerModule.java
new file mode 100644
index 0000000..d974e20
--- /dev/null
+++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTunerModule.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.sample.network.app;
+
+import com.android.tv.common.flags.impl.DefaultFlagsModule;
+import com.android.tv.tuner.api.TunerFactory;
+import com.android.tv.tuner.builtin.BuiltInTunerHalFactory;
+import com.android.tv.tuner.modules.TunerModule;
+import com.android.tv.tuner.sample.network.setup.SampleNetworkTunerSetupActivity;
+import com.android.tv.tuner.sample.network.tvinput.SampleNetworkTunerTvInputService;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactory;
+import dagger.Module;
+import dagger.Provides;
+
+/** Dagger module for {@link SampleNetworkTuner}. */
+@Module(
+        includes = {
+            DefaultFlagsModule.class,
+            SampleNetworkTunerTvInputService.Module.class,
+            SampleNetworkTunerSetupActivity.Module.class,
+            TunerModule.class,
+        })
+class SampleNetworkTunerModule {
+    private final SampleNetworkTuner mSampleNetworkTuner;
+
+    SampleNetworkTunerModule(SampleNetworkTuner sampleNetworkTuner) {
+        mSampleNetworkTuner = sampleNetworkTuner;
+    }
+
+    @Provides
+    public TunerSessionFactory providesTunerSessionFactory() {
+        return mSampleNetworkTuner.getTunerSessionFactory();
+    }
+
+    @Provides
+    TunerFactory providesTunerFactory() {
+        return BuiltInTunerHalFactory.INSTANCE;
+    }
+}
diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/setup/SampleNetworkTunerSetupActivity.java b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/setup/SampleNetworkTunerSetupActivity.java
new file mode 100644
index 0000000..fd783c4
--- /dev/null
+++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/setup/SampleNetworkTunerSetupActivity.java
@@ -0,0 +1,500 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.sample.network.setup;
+
+import android.app.FragmentManager;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.media.tv.TvInputInfo;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+import android.view.KeyEvent;
+import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.singletons.HasSingletons;
+import com.android.tv.common.ui.setup.SetupFragment;
+import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
+import com.android.tv.common.util.PostalCodeUtils;
+import com.android.tv.tuner.sample.network.R;
+import com.android.tv.tuner.setup.BaseTunerSetupActivity;
+import com.android.tv.tuner.setup.ConnectionTypeFragment;
+import com.android.tv.tuner.setup.LineupFragment;
+import com.android.tv.tuner.setup.LocationFragment;
+import com.android.tv.tuner.setup.PostalCodeFragment;
+import com.android.tv.tuner.setup.ScanFragment;
+import com.android.tv.tuner.setup.ScanResultFragment;
+import com.android.tv.tuner.setup.WelcomeFragment;
+import com.android.tv.tuner.singletons.TunerSingletons;
+import com.google.android.tv.partner.support.EpgContract;
+import com.google.android.tv.partner.support.EpgInput;
+import com.google.android.tv.partner.support.EpgInputs;
+import com.google.android.tv.partner.support.Lineup;
+import com.google.android.tv.partner.support.Lineups;
+import com.google.android.tv.partner.support.TunerSetupUtils;
+import dagger.android.ContributesAndroidInjector;
+import java.util.ArrayList;
+import java.util.List;
+
+/** An activity that serves Live TV tuner setup process. */
+public class SampleNetworkTunerSetupActivity extends BaseTunerSetupActivity {
+    private static final String TAG = "SampleNetworkTunerSetupActivity";
+    private static final boolean DEBUG = false;
+
+    private static final int FETCH_LINEUP_TIMEOUT_MS = 10000; // 10 seconds
+    private static final int FETCH_LINEUP_RETRY_TIMEOUT_MS = 20000; // 20 seconds
+    private static final String OTAD_PREFIX = "OTAD";
+    private static final String STRING_BROADCAST_DIGITAL = "Broadcast Digital";
+
+    private LineupFragment currentLineupFragment;
+
+    private List<String> channelNumbers;
+    private List<Lineup> lineups;
+    private Lineup selectedLineup;
+    private List<Pair<Lineup, Integer>> lineupMatchCountPair;
+    private FetchLineupTask fetchLineupTask;
+    private EpgInput epgInput;
+    private String postalCode;
+    private final Handler handler = new Handler();
+    private final Runnable cancelFetchLineupTaskRunnable = this::cancelFetchLineup;
+    private String embeddedInputId;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        if (DEBUG) {
+            Log.d(TAG, "onCreate");
+        }
+        embeddedInputId =
+                HasSingletons.get(TunerSingletons.class, getApplicationContext())
+                        .getEmbeddedTunerInputId();
+        new QueryEpgInputTask(embeddedInputId).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+    }
+
+    @Override
+    protected void executeGetTunerTypeAndCountAsyncTask() {
+        new AsyncTask<Void, Void, Integer>() {
+            @Override
+            protected Integer doInBackground(Void... arg0) {
+                return mTunerFactory.getTunerTypeAndCount(SampleNetworkTunerSetupActivity.this)
+                        .first;
+            }
+
+            @Override
+            protected void onPostExecute(Integer result) {
+                if (!SampleNetworkTunerSetupActivity.this.isDestroyed()) {
+                    mTunerType = result;
+                    if (result == null) {
+                        finish();
+                    } else if (!mActivityStopped) {
+                        showInitialFragment();
+                    } else {
+                        mPendingShowInitialFragment = true;
+                    }
+                }
+            }
+        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+    }
+
+    @Override
+    protected boolean executeAction(String category, int actionId, Bundle params) {
+        switch (category) {
+            case WelcomeFragment.ACTION_CATEGORY:
+                switch (actionId) {
+                    case SetupMultiPaneFragment.ACTION_DONE:
+                        super.executeAction(category, actionId, params);
+                        break;
+                    default:
+                        String postalCode = PostalCodeUtils.getLastPostalCode(this);
+                        boolean needLocation =
+                                CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(
+                                                getApplicationContext())
+                                        && TextUtils.isEmpty(postalCode);
+                        if (needLocation
+                                && checkSelfPermission(
+                                                android.Manifest.permission.ACCESS_COARSE_LOCATION)
+                                        != PackageManager.PERMISSION_GRANTED) {
+                            showLocationFragment();
+                        } else if (mNeedToShowPostalCodeFragment || needLocation) {
+                            // We cannot get postal code automatically. Postal code input fragment
+                            // should always be shown even if users have input some valid postal
+                            // code in this activity before.
+                            mNeedToShowPostalCodeFragment = true;
+                            showPostalCodeFragment();
+                        } else {
+                            lineups = null;
+                            selectedLineup = null;
+                            this.postalCode = postalCode;
+                            restartFetchLineupTask();
+                            showConnectionTypeFragment();
+                        }
+                        break;
+                }
+                return true;
+            case LocationFragment.ACTION_CATEGORY:
+                switch (actionId) {
+                    case LocationFragment.ACTION_ALLOW_PERMISSION:
+                        String postalCode =
+                                params == null
+                                        ? null
+                                        : params.getString(LocationFragment.KEY_POSTAL_CODE);
+                        if (postalCode == null) {
+                            showPostalCodeFragment();
+                        } else {
+                            this.postalCode = postalCode;
+                            restartFetchLineupTask();
+                            showConnectionTypeFragment();
+                        }
+                        break;
+                    default:
+                        cancelFetchLineup();
+                        showConnectionTypeFragment();
+                }
+                return true;
+            case PostalCodeFragment.ACTION_CATEGORY:
+                lineups = null;
+                selectedLineup = null;
+                switch (actionId) {
+                    case SetupMultiPaneFragment.ACTION_DONE:
+                        String postalCode = params.getString(PostalCodeFragment.KEY_POSTAL_CODE);
+                        if (postalCode != null) {
+                            this.postalCode = postalCode;
+                            restartFetchLineupTask();
+                        }
+                        // fall through
+                    case SetupMultiPaneFragment.ACTION_SKIP:
+                        showConnectionTypeFragment();
+                        break;
+                    default: // fall out
+                }
+                return true;
+            case ConnectionTypeFragment.ACTION_CATEGORY:
+                channelNumbers = null;
+                lineupMatchCountPair = null;
+                return super.executeAction(category, actionId, params);
+            case ScanFragment.ACTION_CATEGORY:
+                switch (actionId) {
+                    case ScanFragment.ACTION_CANCEL:
+                        getFragmentManager().popBackStack();
+                        return true;
+                    case ScanFragment.ACTION_FINISH:
+                        clearTunerHal();
+                        channelNumbers =
+                                params.getStringArrayList(ScanFragment.KEY_CHANNEL_NUMBERS);
+                        selectedLineup = null;
+                        if (CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(
+                                        getApplicationContext())
+                                && channelNumbers != null
+                                && !channelNumbers.isEmpty()
+                                && !TextUtils.isEmpty(this.postalCode)) {
+                            showLineupFragment();
+                        } else {
+                            showScanResultFragment();
+                        }
+                        return true;
+                    default: // fall out
+                }
+                break;
+            case LineupFragment.ACTION_CATEGORY:
+                switch (actionId) {
+                    case LineupFragment.ACTION_SKIP:
+                        selectedLineup = null;
+                        currentLineupFragment = null;
+                        showScanResultFragment();
+                        break;
+                    case LineupFragment.ACTION_ID_RETRY:
+                        currentLineupFragment.onRetry();
+                        restartFetchLineupTask();
+                        handler.postDelayed(
+                                cancelFetchLineupTaskRunnable, FETCH_LINEUP_RETRY_TIMEOUT_MS);
+                        break;
+                    default:
+                        if (actionId >= 0 && actionId < lineupMatchCountPair.size()) {
+                            if (DEBUG) {
+                                if (selectedLineup != null) {
+                                    Log.d(
+                                            TAG,
+                                            "Lineup " + selectedLineup.getName() + " is selected.");
+                                }
+                            }
+                            selectedLineup = lineupMatchCountPair.get(actionId).first;
+                        }
+                        currentLineupFragment = null;
+                        showScanResultFragment();
+                        break;
+                }
+                return true;
+            case ScanResultFragment.ACTION_CATEGORY:
+                switch (actionId) {
+                    case SetupMultiPaneFragment.ACTION_DONE:
+                        new InsertOrModifyEpgInputTask(selectedLineup, embeddedInputId)
+                                .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+                        break;
+                    default:
+                        // scan again
+                        if (lineups == null || lineups.isEmpty()) {
+                            lineups = null;
+                            restartFetchLineupTask();
+                        }
+                        super.executeAction(category, actionId, params);
+                        break;
+                }
+                return true;
+            default: // fall out
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (keyCode == KeyEvent.KEYCODE_BACK) {
+            FragmentManager manager = getFragmentManager();
+            int count = manager.getBackStackEntryCount();
+            if (count > 0) {
+                String lastTag = manager.getBackStackEntryAt(count - 1).getName();
+                if (LineupFragment.class.getCanonicalName().equals(lastTag) && count >= 2) {
+                    // Pops fragment including ScanFragment.
+                    manager.popBackStack(
+                            manager.getBackStackEntryAt(count - 2).getName(),
+                            FragmentManager.POP_BACK_STACK_INCLUSIVE);
+                    return true;
+                }
+                if (ScanResultFragment.class.getCanonicalName().equals(lastTag) && count >= 2) {
+                    String secondLastTag = manager.getBackStackEntryAt(count - 2).getName();
+                    if (ScanFragment.class.getCanonicalName().equals(secondLastTag)) {
+                        // Pops fragment including ScanFragment.
+                        manager.popBackStack(
+                                secondLastTag, FragmentManager.POP_BACK_STACK_INCLUSIVE);
+                        return true;
+                    }
+                    if (LineupFragment.class.getCanonicalName().equals(secondLastTag)) {
+                        currentLineupFragment =
+                                (LineupFragment) manager.findFragmentByTag(secondLastTag);
+                        if (lineups == null || lineups.isEmpty()) {
+                            lineups = null;
+                            restartFetchLineupTask();
+                        }
+                    }
+                } else if (ScanFragment.class.getCanonicalName().equals(lastTag)) {
+                    mLastScanFragment.finishScan(true);
+                    return true;
+                }
+            }
+        }
+        return super.onKeyUp(keyCode, event);
+    }
+
+    private void showLineupFragment() {
+        if (lineupMatchCountPair == null && lineups != null) {
+            lineupMatchCountPair = TunerSetupUtils.lineupChannelMatchCount(lineups, channelNumbers);
+        }
+        currentLineupFragment = new LineupFragment();
+        currentLineupFragment.setArguments(getArgsForLineupFragment());
+        currentLineupFragment.setShortDistance(
+                SetupFragment.FRAGMENT_ENTER_TRANSITION | SetupFragment.FRAGMENT_RETURN_TRANSITION);
+        handler.removeCallbacksAndMessages(null);
+        showFragment(currentLineupFragment, true);
+        handler.postDelayed(cancelFetchLineupTaskRunnable, FETCH_LINEUP_TIMEOUT_MS);
+    }
+
+    private Bundle getArgsForLineupFragment() {
+        Bundle args = new Bundle();
+        if (lineupMatchCountPair == null) {
+            return args;
+        }
+        ArrayList<String> lineupNames = new ArrayList<>(lineupMatchCountPair.size());
+        ArrayList<Integer> matchNumbers = new ArrayList<>(lineupMatchCountPair.size());
+        int defaultLineupIndex = 0;
+        for (Pair<Lineup, Integer> pair : lineupMatchCountPair) {
+            Lineup lineup = pair.first;
+            String name;
+            if (!TextUtils.isEmpty(lineup.getName())) {
+                name = lineup.getName();
+            } else {
+                name = lineup.getId();
+            }
+            if (name.equals(OTAD_PREFIX + postalCode) || name.equals(STRING_BROADCAST_DIGITAL)) {
+                // rename OTA / antenna lineups
+                name = getString(R.string.ut_lineup_name_antenna);
+            }
+            lineupNames.add(name);
+            matchNumbers.add(pair.second);
+            if (epgInput != null && TextUtils.equals(lineup.getId(), epgInput.getLineupId())) {
+                // The last index is the current one.
+                defaultLineupIndex = lineupNames.size() - 1;
+            }
+        }
+        args.putStringArrayList(LineupFragment.KEY_LINEUP_NAMES, lineupNames);
+        args.putIntegerArrayList(LineupFragment.KEY_MATCH_NUMBERS, matchNumbers);
+        args.putInt(LineupFragment.KEY_DEFAULT_LINEUP, defaultLineupIndex);
+        return args;
+    }
+
+    private void cancelFetchLineup() {
+        if (fetchLineupTask == null) {
+            return;
+        }
+        AsyncTask.Status status = fetchLineupTask.getStatus();
+        if (status == AsyncTask.Status.RUNNING || status == AsyncTask.Status.PENDING) {
+            fetchLineupTask.cancel(true);
+            fetchLineupTask = null;
+            if (currentLineupFragment != null) {
+                currentLineupFragment.onLineupNotFound();
+            }
+        }
+    }
+
+    private void restartFetchLineupTask() {
+        if (!CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(getApplicationContext())
+                || TextUtils.isEmpty(postalCode)
+                || checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION)
+                        != PackageManager.PERMISSION_GRANTED) {
+            return;
+        }
+        if (fetchLineupTask != null) {
+            fetchLineupTask.cancel(true);
+        }
+        handler.removeCallbacksAndMessages(null);
+        fetchLineupTask = new FetchLineupTask(getContentResolver(), postalCode);
+        fetchLineupTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+    }
+
+    private class FetchLineupTask extends AsyncTask<Void, Void, List<Lineup>> {
+        private final ContentResolver contentResolver;
+        private final String postalCode;
+
+        FetchLineupTask(ContentResolver contentResolver, String postalCode) {
+            this.contentResolver = contentResolver;
+            this.postalCode = postalCode;
+        }
+
+        @Override
+        protected List<Lineup> doInBackground(Void... args) {
+            if (contentResolver == null || TextUtils.isEmpty(postalCode)) {
+                return new ArrayList<>();
+            }
+            return new ArrayList<>(Lineups.query(contentResolver, postalCode));
+        }
+
+        @Override
+        protected void onPostExecute(List<Lineup> lineups) {
+            if (DEBUG) {
+                if (lineups != null) {
+                    Log.d(TAG, "FetchLineupTask fetched " + lineups.size() + " lineups");
+                } else {
+                    Log.d(TAG, "FetchLineupTask returned null");
+                }
+            }
+            SampleNetworkTunerSetupActivity.this.lineups = lineups;
+            if (currentLineupFragment != null) {
+                if (lineups == null || lineups.isEmpty()) {
+                    currentLineupFragment.onLineupNotFound();
+                } else {
+                    lineupMatchCountPair =
+                            TunerSetupUtils.lineupChannelMatchCount(
+                                    SampleNetworkTunerSetupActivity.this.lineups, channelNumbers);
+                    currentLineupFragment.onLineupFound(getArgsForLineupFragment());
+                }
+            }
+        }
+    }
+
+    private class InsertOrModifyEpgInputTask extends AsyncTask<Void, Void, Void> {
+        private final Lineup lineup;
+        private final String inputId;
+
+        InsertOrModifyEpgInputTask(@Nullable Lineup lineup, String inputId) {
+            this.lineup = lineup;
+            this.inputId = inputId;
+        }
+
+        @Override
+        protected Void doInBackground(Void... args) {
+            if (lineup == null
+                    || (SampleNetworkTunerSetupActivity.this.epgInput != null
+                            && TextUtils.equals(
+                                    lineup.getId(),
+                                    SampleNetworkTunerSetupActivity.this.epgInput.getLineupId()))) {
+                return null;
+            }
+            ContentValues values = new ContentValues();
+            values.put(EpgContract.EpgInputs.COLUMN_INPUT_ID, inputId);
+            values.put(EpgContract.EpgInputs.COLUMN_LINEUP_ID, lineup.getId());
+
+            ContentResolver contentResolver = getContentResolver();
+            if (SampleNetworkTunerSetupActivity.this.epgInput != null) {
+                values.put(
+                        EpgContract.EpgInputs.COLUMN_ID,
+                        SampleNetworkTunerSetupActivity.this.epgInput.getId());
+                EpgInputs.update(contentResolver, EpgInput.createEpgChannel(values));
+                return null;
+            }
+            EpgInput epgInput = EpgInputs.queryEpgInput(contentResolver, inputId);
+            if (epgInput == null) {
+                contentResolver.insert(EpgContract.EpgInputs.CONTENT_URI, values);
+            } else {
+                values.put(EpgContract.EpgInputs.COLUMN_ID, epgInput.getId());
+                EpgInputs.update(contentResolver, EpgInput.createEpgChannel(values));
+            }
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(Void result) {
+            Intent data = new Intent();
+            data.putExtra(TvInputInfo.EXTRA_INPUT_ID, inputId);
+            data.putExtra(EpgContract.EXTRA_USE_CLOUD_EPG, true);
+            setResult(RESULT_OK, data);
+            finish();
+        }
+    }
+
+    /**
+     * Exports {@link SampleNetworkTunerSetupActivity} for Dagger codegen to create the appropriate
+     * injector.
+     */
+    @dagger.Module
+    public abstract static class Module {
+        @ContributesAndroidInjector
+        abstract SampleNetworkTunerSetupActivity
+                contributeSampleNetworkTunerSetupActivityInjector();
+    }
+
+    private class QueryEpgInputTask extends AsyncTask<Void, Void, EpgInput> {
+        private final String inputId;
+
+        QueryEpgInputTask(String inputId) {
+            this.inputId = inputId;
+        }
+
+        @Override
+        protected EpgInput doInBackground(Void... args) {
+            ContentResolver contentResolver = getContentResolver();
+            return EpgInputs.queryEpgInput(contentResolver, inputId);
+        }
+
+        @Override
+        protected void onPostExecute(EpgInput result) {
+            epgInput = result;
+        }
+    }
+}
diff --git a/src/com/android/tv/util/Filter.java b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/singletons/SampleNetworkSingletons.java
similarity index 62%
copy from src/com/android/tv/util/Filter.java
copy to tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/singletons/SampleNetworkSingletons.java
index 3e24a49..00a6e27 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/singletons/SampleNetworkSingletons.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.tv.util;
+package com.android.tv.tuner.sample.network.singletons;
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
-}
+import com.android.tv.common.BaseSingletons;
+import com.android.tv.tuner.singletons.TunerSingletons;
+
+/** Singletons for SampleNetworkTuner. */
+public interface SampleNetworkSingletons extends BaseSingletons, TunerSingletons {}
diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/tvinput/SampleNetworkTunerTvInputService.java b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/tvinput/SampleNetworkTunerTvInputService.java
new file mode 100644
index 0000000..de5ff22
--- /dev/null
+++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/tvinput/SampleNetworkTunerTvInputService.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.sample.network.tvinput;
+
+import com.android.tv.tuner.tvinput.BaseTunerTvInputService;
+import dagger.android.ContributesAndroidInjector;
+
+/** Sample DVB Tuner {@link android.media.tv.TvInputService}. */
+public class SampleNetworkTunerTvInputService extends BaseTunerTvInputService {
+
+    /**
+     * Exports {@link SampleNetworkTunerTvInputService} for Dagger codegen to create the appropriate
+     * injector.
+     */
+    @dagger.Module
+    public abstract static class Module {
+        @ContributesAndroidInjector
+        abstract SampleNetworkTunerTvInputService
+                contributesSampleNetworkTunerTvInputServiceInjector();
+    }
+}
diff --git a/tuner/build.gradle b/tuner/build.gradle
new file mode 100644
index 0000000..0f40a29
--- /dev/null
+++ b/tuner/build.gradle
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ * Experimental gradle configuration.  This file may not be up to date.
+ */
+
+apply plugin: 'com.android.library'
+apply plugin: 'com.google.protobuf'
+buildscript {
+    repositories {
+        mavenCentral()
+        google()
+        jcenter()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.1.4'
+        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.5'
+    }
+}
+android {
+    compileSdkVersion 26
+    buildToolsVersion '28.0.2'
+
+    dexOptions {
+        preDexLibraries = false
+        additionalParameters = ['--core-library']
+        javaMaxHeapSize "6g"
+    }
+
+    android {
+        defaultConfig {
+            resConfigs "en"
+        }
+    }
+
+    defaultConfig {
+        minSdkVersion 23
+        targetSdkVersion 26
+        versionCode 1
+        versionName "1.0"
+    }
+    buildTypes {
+        debug {
+            minifyEnabled false
+        }
+        release {
+            minifyEnabled true
+        }
+    }
+    compileOptions() {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+
+    sourceSets {
+        main {
+            res.srcDirs = ['res']
+            java.srcDirs = ['src']
+            manifest.srcFile 'AndroidManifest.xml'
+            proto {
+                srcDir 'proto/'
+            }
+        }
+    }
+}
+
+repositories {
+    mavenCentral()
+    google()
+    jcenter()
+}
+
+final String SUPPORT_LIBS_VERSION = '26.1.0'
+dependencies {
+    implementation 'com.google.android.exoplayer:exoplayer-core:2.9.0'
+    implementation 'com.google.android.exoplayer:exoplayer:r1.5.16'
+    implementation "com.android.support:support-tv-provider:${SUPPORT_LIBS_VERSION}"
+    implementation "com.android.support:appcompat-v7:${SUPPORT_LIBS_VERSION}"
+    implementation "com.android.support:leanback-v17:${SUPPORT_LIBS_VERSION}"
+    implementation 'com.google.guava:guava:26.0-android'
+    implementation 'com.google.protobuf.nano:protobuf-javanano:3.2.0rc2'
+    implementation project(':common')
+}
+protobuf {
+    // Configure the protoc executable
+    protoc {
+        artifact = 'com.google.protobuf:protoc:3.1.0'
+
+        generateProtoTasks {
+            all().each {
+                task -> task.builtins {
+                    remove java
+                    javanano {
+                        option "enum_style=java"
+                    }
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tuner/buildconfig.mk b/tuner/buildconfig.mk
deleted file mode 100644
index cece7f2..0000000
--- a/tuner/buildconfig.mk
+++ /dev/null
@@ -1,39 +0,0 @@
-#
-# Copyright (C) 2015 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-# Emulate gradles BuildConfig.java
-
-ifeq "$(TARGET_BUILD_VARIANT)" "eng"
-   BC_DEBUG_STATUS := true
-else ifeq "$(TARGET_BUILD_VARIANT)" "userdebug"
-   BC_DEBUG_STATUS := true
-else
-   BC_DEBUG_STATUS := false
-endif
-
-ifeq "$(TARGET_BUILD_VARIANT)" "eng"
-   BC_ENG_STATUS := true
-else
-   BC_ENG_STATUS := false
-endif
-
-gen := $(local-generated-sources-dir)/$(TARGET_BUILD_VARIANT)/BuildConfig.java
-$(gen): PRIVATE_CUSTOM_TOOL = sed -e \
-        's/%DEBUG%/$(BC_DEBUG_STATUS)/;s/%ENG%/$(BC_ENG_STATUS)/' \
-        $< > $@
-$(gen) : $(LOCAL_PATH)/BuildConfig.java.in
-	$(transform-generated-source)
-LOCAL_GENERATED_SOURCES += $(gen)
\ No newline at end of file
diff --git a/tuner/proto/Android.bp b/tuner/proto/Android.bp
new file mode 100644
index 0000000..67f35f8
--- /dev/null
+++ b/tuner/proto/Android.bp
@@ -0,0 +1,27 @@
+//
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+java_library {
+    name: "live-tv-tuner-proto",
+    srcs: ["*.proto"],
+    sdk_version: "system_current",
+    proto: {
+        type: "nano",
+        output_params: ["enum_style=java"],
+        canonical_path_from_root: false,
+    },
+    min_sdk_version: "23",
+}
diff --git a/tuner/proto/channel.proto b/tuner/proto/channel.proto
index 1f99452..ff372ad 100644
--- a/tuner/proto/channel.proto
+++ b/tuner/proto/channel.proto
@@ -21,10 +21,15 @@
 option java_package = "com.android.tv.tuner.data";
 option java_outer_classname = "Channel";
 
+
+// AOSP_Comment_Out import "third_party/android/nanoproto/nano_descriptor.proto";
+
 import "track.proto";
 
 // Holds information about a channel used in the tuners.
 message TunerChannelProto {
+// AOSP_Comment_Out   option (proto2.nano.message_as_lite) = false;
+
   optional TunerType type = 1;
   optional string short_name = 2;
   optional string long_name = 3;
@@ -60,6 +65,8 @@
 
 // Enum describing the types of tuner.
 enum TunerType {
+// AOSP_Comment_Out   option (proto2.nano.enum_as_lite) = false;
+
   TYPE_TUNER = 0;
   TYPE_FILE = 1;
   TYPE_NETWORK = 2;
@@ -67,6 +74,8 @@
 
 // Enum describing the types of video stream.
 enum VideoStreamType {
+// AOSP_Comment_Out   option (proto2.nano.enum_as_lite) = false;
+
   // ISO/IEC 11172 Video (MPEG-1)
   MPEG1 = 0x01;
   // ISO/IEC 13818-2 (MPEG-2) Video
@@ -81,6 +90,8 @@
 
 // Enum describing the types of audio stream.
 enum AudioStreamType {
+// AOSP_Comment_Out   option (proto2.nano.enum_as_lite) = false;
+
   // ISO/IEC 11172 Audio (MPEG-1)
   MPEG1AUDIO = 0x03;
   // ISO/IEC 13818-3 Audio (MPEG-2)
@@ -98,6 +109,8 @@
 // Enum describing ATSC service types
 // See ATSC Code Points Registry.
 enum AtscServiceType {
+// AOSP_Comment_Out   option (proto2.nano.enum_as_lite) = false;
+
   SERVICE_TYPE_ATSC_RESERVED = 0x0;
   SERVICE_TYPE_ANALOG_TELEVISION_CHANNELS = 0x1;
   SERVICE_TYPE_ATSC_DIGITAL_TELEVISION = 0x2;
diff --git a/tuner/proto/track.proto b/tuner/proto/track.proto
index fe60fed..11ca784 100644
--- a/tuner/proto/track.proto
+++ b/tuner/proto/track.proto
@@ -18,11 +18,15 @@
 
 package com.android.tv.tuner.data;
 
+// AOSP_Comment_Out import "third_party/android/nanoproto/nano_descriptor.proto";
+
 option java_package = "com.android.tv.tuner.data";
 option java_outer_classname = "Track";
 
 // Represents a AC3 audio track.
 message AtscAudioTrack {
+// AOSP_Comment_Out   option (proto2.nano.message_as_lite) = false; 
+
   optional string language = 1;
   optional AudioType audio_type = 2;
   optional int32 index = 3;
@@ -32,6 +36,8 @@
   // Enum describing the types of a audio track.
   // See ISO/IEC 138181-1:2000(e) Table 2-53.
   enum AudioType {
+// AOSP_Comment_Out     option (proto2.nano.enum_as_lite) = false;
+
     AUDIOTYPE_UNDEFINED = 0;
     AUDIOTYPE_CLEAN_EFFECTS = 1;
     AUDIOTYPE_HEARING_IMPAIRED = 2;
@@ -41,6 +47,8 @@
 
 // Represents a CEA-708 caption track.
 message AtscCaptionTrack {
+// AOSP_Comment_Out   option (proto2.nano.message_as_lite) = false;
+
   optional string language = 1;
   optional int32 service_number = 2;
   optional bool easy_reader = 3;
diff --git a/tuner/res/values/strings.xml b/tuner/res/values/strings.xml
index 58d7214..96aca8a 100644
--- a/tuner/res/values/strings.xml
+++ b/tuner/res/values/strings.xml
@@ -210,10 +210,40 @@
     <!-- Title of postal/zip code input guided step fragment  [CHAR LIMIT=30] -->
     <string name="postal_code_guidance_title">Enter your ZIP Code.</string>
     <!-- Description of postal/zip code input guided step fragment  [CHAR LIMIT=NONE] -->
-    <string name="postal_code_guidance_description">Live TV app will use the ZIP Code to provide a complete program guide for the TV channels.</string>
+    <string name="postal_code_guidance_description">
+        Please enter your ZIP code.
+        <xlgiff id="break">\n</xlgiff>
+        Your ZIP code is stored on device locally and sent to Google servers when updating your
+        program information. The ZIP code sent is never associated with your account or stored in
+        Google servers.
+        <xlgiff id="break">\n</xlgiff>
+        If you do not provide your ZIP code, your TV program guide will be limited.</string>
+    <!-- Description prefix of postal/zip code input guided step fragment when failed to get location automatically. [CHAR LIMIT=NONE] -->
+    <string name="postal_code_guidance_description_get_location_failed">
+        <xliff:g id="app_name">Live TV</xliff:g> cannot determine your location.</string>
     <!-- Description of postal/zip code input edit text view to prompt users entering ZIP Code  [CHAR LIMIT=30] -->
     <string name="postal_code_action_description">Enter your ZIP Code</string>
     <!-- Warning message shown in description field of postal/zip code input edit text view when user enters an invalid ZIP Code and presses Done [CHAR LIMIT=30] -->
     <string name="postal_code_invalid_warning">Invalid ZIP Code</string>
 
+    <!-- Title of location rationale guided step fragment  [CHAR LIMIT=30] -->
+    <string name="location_guidance_title">Location</string>
+    <!-- Description of location rationale guided step fragment  [CHAR LIMIT=NONE] -->
+    <string name="location_guidance_description">
+        <xliff:g id="app_name">Live TV</xliff:g> uses your ZIP code to provide a complete
+        program guide for your TV channels.
+        <xlgiff id="break">\n</xlgiff>
+        Your ZIP code is stored on device locally and sent to Google servers when updating your
+        program information. The ZIP code sent is never associated with your account or stored in
+        Google servers.
+        <xlgiff id="break">\n</xlgiff>
+        If you do not grant permission or do not manually enter your ZIP code, your TV program guide
+        will be limited.</string>
+    <!-- Grant location permission -->
+    <string name="location_choices_allow_permission">Grant Permission</string>
+    <!-- Enter postal code -->
+    <string name="location_choices_enter_zip_code">Enter Zip Code</string>
+    <!-- Message to show users when getting location information -->
+    <string name="location_choices_getting_location">Getting Location</string>
+
 </resources>
diff --git a/tuner/src/com/android/tv/tuner/DvbTunerHal.java b/tuner/src/com/android/tv/tuner/DvbTunerHal.java
index 4375fc3..c802ebb 100644
--- a/tuner/src/com/android/tv/tuner/DvbTunerHal.java
+++ b/tuner/src/com/android/tv/tuner/DvbTunerHal.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.os.ParcelFileDescriptor;
 import android.util.Log;
+import com.android.tv.common.compat.TvInputConstantCompat;
 import com.android.tv.tuner.DvbDeviceAccessor.DvbDeviceInfoWrapper;
 import java.util.List;
 import java.util.SortedSet;
@@ -26,13 +27,18 @@
 
 /** A class to handle a hardware Linux DVB API supported tuner device. */
 public class DvbTunerHal extends TunerHal {
+    private static final String TAG = "DvbTunerHal";
+    private static final boolean DEBUG = false;
 
     private static final Object sLock = new Object();
     // @GuardedBy("sLock")
     private static final SortedSet<DvbDeviceInfoWrapper> sUsedDvbDevices = new TreeSet<>();
+    // The minimum delta for updating signal strength when valid
+    private static final int SIGNAL_STRENGTH_MINIMUM_DELTA = 2;
 
     private final DvbDeviceAccessor mDvbDeviceAccessor;
     private DvbDeviceInfoWrapper mDvbDeviceInfo;
+    private int mSignalStrength = TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED;
 
     public DvbTunerHal(Context context) {
         super(context);
@@ -40,7 +46,7 @@
     }
 
     @Override
-    protected boolean openFirstAvailable() {
+    public boolean openFirstAvailable() {
         List<DvbDeviceInfoWrapper> deviceInfoList = mDvbDeviceAccessor.getDvbDeviceList();
         if (deviceInfoList == null || deviceInfoList.isEmpty()) {
             Log.e(TAG, "There's no dvb device attached");
@@ -115,12 +121,12 @@
     }
 
     @Override
-    protected boolean isDeviceOpen() {
+    public boolean isDeviceOpen() {
         return (mDvbDeviceInfo != null);
     }
 
     @Override
-    protected long getDeviceId() {
+    public long getDeviceId() {
         if (mDvbDeviceInfo != null) {
             return mDvbDeviceInfo.getId();
         }
@@ -174,4 +180,45 @@
             return 0;
         }
     }
+
+    @Override
+    public int getSignalStrength() {
+        int signalStrength;
+        signalStrength = nativeGetSignalStrength(getDeviceId());
+        if (signalStrength == -3) {
+            mSignalStrength = signalStrength;
+            return TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED;
+        }
+        if (signalStrength > 65535 || signalStrength < 0) {
+            mSignalStrength = signalStrength;
+            return TvInputConstantCompat.SIGNAL_STRENGTH_ERROR;
+        }
+        signalStrength = getCurvedSignalStrength(signalStrength);
+        return updatingSignal(signalStrength);
+    }
+
+    /**
+     * This method curves the raw signal strength from tuner when it's between 0 - 65535 inclusive.
+     */
+    private int getCurvedSignalStrength(int signalStrength) {
+        /** When value < 80% of 65535, it will be recognized as level 0. */
+        if (signalStrength < 65535 * 0.8) {
+            return 0;
+        }
+        /** When value is between 80% to 100% of 65535, it will be linearly mapped to 0 - 100%. */
+        return (int) (5 * (signalStrength * 100.0 / 65535) - 400);
+    }
+
+    /**
+     * This method is for noise canceling. If the delta between current and previous strength is
+     * less than {@link #SIGNAL_STRENGTH_MINIMUM_DELTA}, previous signal strength will be returned.
+     * Otherwise current signal strength will be updated and returned.
+     */
+    private int updatingSignal(int signal) {
+        int delta = Math.abs(signal - mSignalStrength);
+        if (delta > SIGNAL_STRENGTH_MINIMUM_DELTA) {
+            mSignalStrength = signal;
+        }
+        return mSignalStrength;
+    }
 }
diff --git a/tuner/src/com/android/tv/tuner/TunerFeatures.java b/tuner/src/com/android/tv/tuner/TunerFeatures.java
deleted file mode 100644
index e682e63..0000000
--- a/tuner/src/com/android/tv/tuner/TunerFeatures.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.tv.tuner;
-
-import static com.android.tv.common.feature.FeatureUtils.OFF;
-
-import android.content.Context;
-import android.text.TextUtils;
-import android.util.Log;
-import com.android.tv.common.BaseApplication;
-import com.android.tv.common.config.api.RemoteConfig;
-import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.common.feature.Feature;
-import com.android.tv.common.feature.Model;
-import com.android.tv.common.feature.PropertyFeature;
-import com.android.tv.common.util.CommonUtils;
-import com.android.tv.common.util.LocationUtils;
-import java.util.Locale;
-
-/**
- * List of {@link Feature} for Tuner.
- *
- * <p>Remove the {@code Feature} once it is launched.
- */
-public class TunerFeatures extends CommonFeatures {
-    private static final String TAG = "TunerFeatures";
-    private static final boolean DEBUG = false;
-
-    /** Use network tuner if it is available and there is no other tuner types. */
-    public static final Feature NETWORK_TUNER =
-            new Feature() {
-                @Override
-                public boolean isEnabled(Context context) {
-                    if (!TUNER.isEnabled(context)) {
-                        return false;
-                    }
-                    if (CommonUtils.isDeveloper()) {
-                        // Network tuner will be enabled for developers.
-                        return true;
-                    }
-                    return Locale.US
-                            .getCountry()
-                            .equalsIgnoreCase(LocationUtils.getCurrentCountry(context));
-                }
-            };
-
-    /**
-     * USE_SW_CODEC_FOR_SD
-     *
-     * <p>Prefer software based codec for SD channels.
-     */
-    public static final Feature USE_SW_CODEC_FOR_SD =
-            PropertyFeature.create(
-                    "use_sw_codec_for_sd",
-                    false
-                    );
-
-    /** Use AC3 software decode. */
-    public static final Feature AC3_SOFTWARE_DECODE =
-            new Feature() {
-                private final String[] SUPPORTED_REGIONS = {};
-
-                private Boolean mEnabled;
-
-                @Override
-                public boolean isEnabled(Context context) {
-                    if (mEnabled == null) {
-                        if (mEnabled == null) {
-                            // We will not cache the result of fallback solution.
-                            String country = LocationUtils.getCurrentCountry(context);
-                            for (int i = 0; i < SUPPORTED_REGIONS.length; ++i) {
-                                if (SUPPORTED_REGIONS[i].equalsIgnoreCase(country)) {
-                                    return true;
-                                }
-                            }
-                            if (DEBUG) Log.d(TAG, "AC3 flag false after country check");
-                            return false;
-                        }
-                    }
-                    if (DEBUG) Log.d(TAG, "AC3 flag " + mEnabled);
-                    return mEnabled;
-                }
-            };
-
-    /** Enable Dvb parsers and listeners. */
-    public static final Feature ENABLE_FILE_DVB = OFF;
-
-    private TunerFeatures() {}
-}
diff --git a/tuner/src/com/android/tv/tuner/TunerHal.java b/tuner/src/com/android/tv/tuner/TunerHal.java
index 5801406..dce4f4c 100644
--- a/tuner/src/com/android/tv/tuner/TunerHal.java
+++ b/tuner/src/com/android/tv/tuner/TunerHal.java
@@ -17,87 +17,25 @@
 package com.android.tv.tuner;
 
 import android.content.Context;
-import android.support.annotation.IntDef;
-import android.support.annotation.StringDef;
-import android.support.annotation.WorkerThread;
 import android.util.Log;
-import android.util.Pair;
 import com.android.tv.common.BuildConfig;
-import com.android.tv.common.customization.CustomizationManager;
-
-
+import com.android.tv.common.compat.TvInputConstantCompat;
+import com.android.tv.tuner.api.Tuner;
 import com.android.tv.common.annotation.UsedByNative;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
 import java.util.Objects;
 
 /** A base class to handle a hardware tuner device. */
-public abstract class TunerHal implements AutoCloseable {
-    protected static final String TAG = "TunerHal";
-    protected static final boolean DEBUG = false;
+public abstract class TunerHal implements Tuner {
+    private static final String TAG = "TunerHal";
 
-    @IntDef({FILTER_TYPE_OTHER, FILTER_TYPE_AUDIO, FILTER_TYPE_VIDEO, FILTER_TYPE_PCR})
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface FilterType {}
+    private static final int PID_PAT = 0;
+    private static final int PID_ATSC_SI_BASE = 0x1ffb;
+    private static final int PID_DVB_SDT = 0x0011;
+    private static final int PID_DVB_EIT = 0x0012;
+    private static final int DEFAULT_VSB_TUNE_TIMEOUT_MS = 2000;
+    private static final int DEFAULT_QAM_TUNE_TIMEOUT_MS = 4000; // Some device takes time for
 
-    public static final int FILTER_TYPE_OTHER = 0;
-    public static final int FILTER_TYPE_AUDIO = 1;
-    public static final int FILTER_TYPE_VIDEO = 2;
-    public static final int FILTER_TYPE_PCR = 3;
-
-    @StringDef({MODULATION_8VSB, MODULATION_QAM256})
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface ModulationType {}
-
-    public static final String MODULATION_8VSB = "8VSB";
-    public static final String MODULATION_QAM256 = "QAM256";
-
-    @IntDef({
-        DELIVERY_SYSTEM_UNDEFINED,
-        DELIVERY_SYSTEM_ATSC,
-        DELIVERY_SYSTEM_DVBC,
-        DELIVERY_SYSTEM_DVBS,
-        DELIVERY_SYSTEM_DVBS2,
-        DELIVERY_SYSTEM_DVBT,
-        DELIVERY_SYSTEM_DVBT2
-    })
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface DeliverySystemType {}
-
-    public static final int DELIVERY_SYSTEM_UNDEFINED = 0;
-    public static final int DELIVERY_SYSTEM_ATSC = 1;
-    public static final int DELIVERY_SYSTEM_DVBC = 2;
-    public static final int DELIVERY_SYSTEM_DVBS = 3;
-    public static final int DELIVERY_SYSTEM_DVBS2 = 4;
-    public static final int DELIVERY_SYSTEM_DVBT = 5;
-    public static final int DELIVERY_SYSTEM_DVBT2 = 6;
-
-    @IntDef({TUNER_TYPE_BUILT_IN, TUNER_TYPE_USB, TUNER_TYPE_NETWORK})
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface TunerType {}
-
-    public static final int TUNER_TYPE_BUILT_IN = 1;
-    public static final int TUNER_TYPE_USB = 2;
-    public static final int TUNER_TYPE_NETWORK = 3;
-
-    protected static final int PID_PAT = 0;
-    protected static final int PID_ATSC_SI_BASE = 0x1ffb;
-    protected static final int PID_DVB_SDT = 0x0011;
-    protected static final int PID_DVB_EIT = 0x0012;
-    protected static final int DEFAULT_VSB_TUNE_TIMEOUT_MS = 2000;
-    protected static final int DEFAULT_QAM_TUNE_TIMEOUT_MS = 4000; // Some device takes time for
-    // QAM256 tuning.
-    @IntDef({
-        BUILT_IN_TUNER_TYPE_LINUX_DVB
-    })
-    @Retention(RetentionPolicy.SOURCE)
-    private @interface BuiltInTunerType {}
-
-    private static final int BUILT_IN_TUNER_TYPE_LINUX_DVB = 1;
-
-    private static Integer sBuiltInTunerType;
-
-    protected @DeliverySystemType int mDeliverySystemType;
+    @DeliverySystemType private int mDeliverySystemType;
     private boolean mIsStreaming;
     private int mFrequency;
     private String mModulation;
@@ -108,66 +46,6 @@
         }
     }
 
-    /**
-     * Creates a TunerHal instance.
-     *
-     * @param context context for creating the TunerHal instance
-     * @return the TunerHal instance
-     */
-    @WorkerThread
-    public static synchronized TunerHal createInstance(Context context) {
-        TunerHal tunerHal = null;
-        if (DvbTunerHal.getNumberOfDevices(context) > 0) {
-            if (DEBUG) Log.d(TAG, "Use DvbTunerHal");
-            tunerHal = new DvbTunerHal(context);
-        }
-        return tunerHal != null && tunerHal.openFirstAvailable() ? tunerHal : null;
-    }
-
-    /** Gets the number of tuner devices currently present. */
-    @WorkerThread
-    public static Pair<Integer, Integer> getTunerTypeAndCount(Context context) {
-        if (useBuiltInTuner(context)) {
-            if (getBuiltInTunerType(context) == BUILT_IN_TUNER_TYPE_LINUX_DVB) {
-                return new Pair<>(TUNER_TYPE_BUILT_IN, DvbTunerHal.getNumberOfDevices(context));
-            }
-        } else {
-            int usbTunerCount = DvbTunerHal.getNumberOfDevices(context);
-            if (usbTunerCount > 0) {
-                return new Pair<>(TUNER_TYPE_USB, usbTunerCount);
-            }
-        }
-        return new Pair<>(null, 0);
-    }
-
-    /** Check a delivery system is for DVB or not. */
-    public static boolean isDvbDeliverySystem(@DeliverySystemType int deliverySystemType) {
-        return deliverySystemType == DELIVERY_SYSTEM_DVBC
-                || deliverySystemType == DELIVERY_SYSTEM_DVBS
-                || deliverySystemType == DELIVERY_SYSTEM_DVBS2
-                || deliverySystemType == DELIVERY_SYSTEM_DVBT
-                || deliverySystemType == DELIVERY_SYSTEM_DVBT2;
-    }
-
-    /**
-     * Returns if tuner input service would use built-in tuners instead of USB tuners or network
-     * tuners.
-     */
-    public static boolean useBuiltInTuner(Context context) {
-        return getBuiltInTunerType(context) != 0;
-    }
-
-    private static @BuiltInTunerType int getBuiltInTunerType(Context context) {
-        if (sBuiltInTunerType == null) {
-            sBuiltInTunerType = 0;
-            if (CustomizationManager.hasLinuxDvbBuiltInTuner(context)
-                    && DvbTunerHal.getNumberOfDevices(context) > 0) {
-                sBuiltInTunerType = BUILT_IN_TUNER_TYPE_LINUX_DVB;
-            }
-        }
-        return sBuiltInTunerType;
-    }
-
     protected TunerHal(Context context) {
         mIsStreaming = false;
         mFrequency = -1;
@@ -188,6 +66,7 @@
      * Returns {@code true} if this tuner HAL can be reused to save tuning time between channels of
      * the same frequency.
      */
+    @Override
     public boolean isReusable() {
         return true;
     }
@@ -201,18 +80,6 @@
     protected native void nativeFinalize(long deviceId);
 
     /**
-     * Acquires the first available tuner device. If there is a tuner device that is available, the
-     * tuner device will be locked to the current instance.
-     *
-     * @return {@code true} if the operation was successful, {@code false} otherwise
-     */
-    protected abstract boolean openFirstAvailable();
-
-    protected abstract boolean isDeviceOpen();
-
-    protected abstract long getDeviceId();
-
-    /**
      * Sets the tuner channel. This should be called after acquiring a tuner device.
      *
      * @param frequency a frequency of the channel to tune to
@@ -221,6 +88,7 @@
      *     use channelNumber instead of frequency for tune.
      * @return {@code true} if the operation was successful, {@code false} otherwise
      */
+    @Override
     public synchronized boolean tune(
             int frequency, @ModulationType String modulation, String channelNumber) {
         if (!isDeviceOpen()) {
@@ -237,7 +105,7 @@
         if (mFrequency == frequency && Objects.equals(mModulation, modulation)) {
             addPidFilter(PID_PAT, FILTER_TYPE_OTHER);
             addPidFilter(PID_ATSC_SI_BASE, FILTER_TYPE_OTHER);
-            if (isDvbDeliverySystem(mDeliverySystemType)) {
+            if (Tuner.isDvbDeliverySystem(mDeliverySystemType)) {
                 addPidFilter(PID_DVB_SDT, FILTER_TYPE_OTHER);
                 addPidFilter(PID_DVB_EIT, FILTER_TYPE_OTHER);
             }
@@ -251,7 +119,7 @@
         if (nativeTune(getDeviceId(), frequency, modulation, timeout_ms)) {
             addPidFilter(PID_PAT, FILTER_TYPE_OTHER);
             addPidFilter(PID_ATSC_SI_BASE, FILTER_TYPE_OTHER);
-            if (isDvbDeliverySystem(mDeliverySystemType)) {
+            if (Tuner.isDvbDeliverySystem(mDeliverySystemType)) {
                 addPidFilter(PID_DVB_SDT, FILTER_TYPE_OTHER);
                 addPidFilter(PID_DVB_EIT, FILTER_TYPE_OTHER);
             }
@@ -273,6 +141,7 @@
      * @param filterType a type of pid. Must be one of (FILTER_TYPE_XXX)
      * @return {@code true} if the operation was successful, {@code false} otherwise
      */
+    @Override
     public synchronized boolean addPidFilter(int pid, @FilterType int filterType) {
         if (!isDeviceOpen()) {
             Log.e(TAG, "There's no available device");
@@ -293,10 +162,13 @@
 
     protected native int nativeGetDeliverySystemType(long deviceId);
 
+    protected native int nativeGetSignalStrength(long deviceId);
+
     /**
      * Stops current tuning. The tuner device and pid filters will be reset by this call and make
      * the tuner ready to accept another tune request.
      */
+    @Override
     public synchronized void stopTune() {
         if (isDeviceOpen()) {
             if (mIsStreaming) {
@@ -309,10 +181,12 @@
         mModulation = null;
     }
 
+    @Override
     public void setHasPendingTune(boolean hasPendingTune) {
         nativeSetHasPendingTune(getDeviceId(), hasPendingTune);
     }
 
+    @Override
     public int getDeliverySystemType() {
         return mDeliverySystemType;
     }
@@ -320,9 +194,9 @@
     protected native void nativeStopTune(long deviceId);
 
     /**
-     * This method must be called after {@link TunerHal#tune} and before {@link TunerHal#stopTune}.
-     * Writes at most maxSize TS frames in a buffer provided by the user. The frames employ MPEG
-     * encoding.
+     * This method must be called after {@link #tune(int, String, String)} and before {@link
+     * #stopTune()}. Writes at most maxSize TS frames in a buffer provided by the user. The frames
+     * employ MPEG encoding.
      *
      * @param javaBuffer a buffer to write the video data in
      * @param javaBufferSize the max amount of bytes to write in this buffer. Usually this number
@@ -330,6 +204,7 @@
      * @return the amount of bytes written in the buffer. Note that this value could be 0 if no new
      *     frames have been obtained since the last call.
      */
+    @Override
     public synchronized int readTsStream(byte[] javaBuffer, int javaBufferSize) {
         if (isDeviceOpen()) {
             return nativeWriteInBuffer(getDeviceId(), javaBuffer, javaBufferSize);
@@ -338,6 +213,21 @@
         }
     }
 
+    /**
+     * This method gets signal strength for currently tuned channel.
+     * Each specific tuner should implement its own method.
+     *
+     * @return {@link TvInputConstantCompat#SIGNAL_STRENGTH_NOT_USED
+     *          when signal check is not supported from tuner.
+     *          {@link TvInputConstantCompat#SIGNAL_STRENGTH_ERROR}
+     *          when signal returned is not valid.
+     *          0 - 100 representing strength from low to high. Curve raw data if necessary.
+     */
+    @Override
+    public int getSignalStrength() {
+        return TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED;
+    }
+
     protected native int nativeWriteInBuffer(long deviceId, byte[] javaBuffer, int javaBufferSize);
 
     /**
diff --git a/tuner/src/com/android/tv/tuner/api/ChannelScanListener.java b/tuner/src/com/android/tv/tuner/api/ChannelScanListener.java
new file mode 100644
index 0000000..e0319a2
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/api/ChannelScanListener.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.api;
+
+import com.android.tv.tuner.data.TunerChannel;
+
+/** Listener for detecting TV channels. */
+public interface ChannelScanListener {
+
+    /**
+     * Fired when new information of an TV channel arrives.
+     *
+     * @param channel an  TV channel
+     * @param channelArrivedAtFirstTime tells whether this channel arrived at first time
+     */
+    void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime);
+}
diff --git a/tuner/src/com/android/tv/tuner/api/ScanChannel.java b/tuner/src/com/android/tv/tuner/api/ScanChannel.java
new file mode 100644
index 0000000..56e5493
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/api/ScanChannel.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.api;
+
+import com.android.tv.tuner.data.nano.Channel;
+
+/** Channel information gathered from a <em>scan</em> */
+public final class ScanChannel {
+    public final int type;
+    public final int frequency;
+    public final String modulation;
+    public final String filename;
+    /**
+     * Radio frequency (channel) number specified at
+     * https://en.wikipedia.org/wiki/North_American_television_frequencies This can be {@code null}
+     * for cases like cable signal.
+     */
+    public final Integer radioFrequencyNumber;
+
+    public static ScanChannel forTuner(
+            int frequency, String modulation, Integer radioFrequencyNumber) {
+        return new ScanChannel(
+                Channel.TunerType.TYPE_TUNER, frequency, modulation, null, radioFrequencyNumber);
+    }
+
+    public static ScanChannel forFile(int frequency, String filename) {
+        return new ScanChannel(Channel.TunerType.TYPE_FILE, frequency, "file:", filename, null);
+    }
+
+    private ScanChannel(
+            int type,
+            int frequency,
+            String modulation,
+            String filename,
+            Integer radioFrequencyNumber) {
+        this.type = type;
+        this.frequency = frequency;
+        this.modulation = modulation;
+        this.filename = filename;
+        this.radioFrequencyNumber = radioFrequencyNumber;
+    }
+}
diff --git a/tuner/src/com/android/tv/tuner/api/Tuner.java b/tuner/src/com/android/tv/tuner/api/Tuner.java
new file mode 100644
index 0000000..6f7e9d9
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/api/Tuner.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.api;
+
+import android.support.annotation.IntDef;
+import android.support.annotation.StringDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** A interface a hardware tuner device. */
+public interface Tuner extends AutoCloseable {
+
+    int FILTER_TYPE_OTHER = 0;
+    int FILTER_TYPE_AUDIO = 1;
+    int FILTER_TYPE_VIDEO = 2;
+    int FILTER_TYPE_PCR = 3;
+    String MODULATION_8VSB = "8VSB";
+    String MODULATION_QAM256 = "QAM256";
+    int DELIVERY_SYSTEM_UNDEFINED = 0;
+    int DELIVERY_SYSTEM_ATSC = 1;
+    int DELIVERY_SYSTEM_DVBC = 2;
+    int DELIVERY_SYSTEM_DVBS = 3;
+    int DELIVERY_SYSTEM_DVBS2 = 4;
+    int DELIVERY_SYSTEM_DVBT = 5;
+    int DELIVERY_SYSTEM_DVBT2 = 6;
+    int TUNER_TYPE_BUILT_IN = 1;
+    int TUNER_TYPE_USB = 2;
+    int TUNER_TYPE_NETWORK = 3;
+    int BUILT_IN_TUNER_TYPE_LINUX_DVB = 1;
+
+    /** Check a delivery system is for DVB or not. */
+    static boolean isDvbDeliverySystem(@DeliverySystemType int deliverySystemType) {
+        return deliverySystemType == DELIVERY_SYSTEM_DVBC
+                || deliverySystemType == DELIVERY_SYSTEM_DVBS
+                || deliverySystemType == DELIVERY_SYSTEM_DVBS2
+                || deliverySystemType == DELIVERY_SYSTEM_DVBT
+                || deliverySystemType == DELIVERY_SYSTEM_DVBT2;
+    }
+
+    boolean isReusable();
+
+    /**
+     * Acquires the first available tuner device. If there is a tuner device that is available, the
+     * tuner device will be locked to the current instance.
+     *
+     * @return {@code true} if the operation was successful, {@code false} otherwise
+     */
+    boolean openFirstAvailable();
+
+    boolean isDeviceOpen();
+
+    long getDeviceId();
+
+    boolean tune(int frequency, @ModulationType String modulation, String channelNumber);
+
+    boolean addPidFilter(int pid, @FilterType int filterType);
+
+    void stopTune();
+
+    void setHasPendingTune(boolean hasPendingTune);
+
+    int getDeliverySystemType();
+
+    int readTsStream(byte[] javaBuffer, int javaBufferSize);
+
+    int getSignalStrength();
+
+    /** Filter type */
+    @IntDef({FILTER_TYPE_OTHER, FILTER_TYPE_AUDIO, FILTER_TYPE_VIDEO, FILTER_TYPE_PCR})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface FilterType {}
+
+    /** Modulation Type */
+    @StringDef({MODULATION_8VSB, MODULATION_QAM256})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ModulationType {}
+
+    /** Delivery System Type */
+    @IntDef({
+        DELIVERY_SYSTEM_UNDEFINED,
+        DELIVERY_SYSTEM_ATSC,
+        DELIVERY_SYSTEM_DVBC,
+        DELIVERY_SYSTEM_DVBS,
+        DELIVERY_SYSTEM_DVBS2,
+        DELIVERY_SYSTEM_DVBT,
+        DELIVERY_SYSTEM_DVBT2
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DeliverySystemType {}
+
+    /** Tuner Type */
+    @IntDef({TUNER_TYPE_BUILT_IN, TUNER_TYPE_USB, TUNER_TYPE_NETWORK})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface TunerType {}
+
+    /** Built in tuner type */
+    @IntDef({
+        BUILT_IN_TUNER_TYPE_LINUX_DVB
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface BuiltInTunerType {}
+}
diff --git a/tuner/src/com/android/tv/tuner/api/TunerFactory.java b/tuner/src/com/android/tv/tuner/api/TunerFactory.java
new file mode 100644
index 0000000..bc29c7c
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/api/TunerFactory.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.api;
+
+import android.content.Context;
+import android.support.annotation.WorkerThread;
+import android.util.Pair;
+
+/** Factory for {@link Tuner}. */
+public interface TunerFactory {
+    @WorkerThread
+    Tuner createInstance(Context context);
+
+    boolean useBuiltInTuner(Context context);
+
+    @WorkerThread
+    Pair<Integer, Integer> getTunerTypeAndCount(Context context);
+}
diff --git a/tuner/src/com/android/tv/tuner/builtin/BuiltInTunerHalFactory.java b/tuner/src/com/android/tv/tuner/builtin/BuiltInTunerHalFactory.java
new file mode 100644
index 0000000..9a0be74
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/builtin/BuiltInTunerHalFactory.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.builtin;
+
+import android.content.Context;
+import android.support.annotation.WorkerThread;
+import android.util.Log;
+import android.util.Pair;
+import com.android.tv.common.customization.CustomizationManager;
+import com.android.tv.common.feature.Model;
+import com.android.tv.tuner.DvbTunerHal;
+import com.android.tv.tuner.api.Tuner;
+import com.android.tv.tuner.api.TunerFactory;
+
+
+/** TunerHal factory that creates all built in tuner types. */
+public final class BuiltInTunerHalFactory implements TunerFactory {
+    private static final String TAG = "BuiltInTunerHalFactory";
+    private static final boolean DEBUG = false;
+
+    private Integer mBuiltInTunerType;
+
+    public static final TunerFactory INSTANCE = new BuiltInTunerHalFactory();
+
+    private BuiltInTunerHalFactory() {}
+
+    @Tuner.BuiltInTunerType
+    private int getBuiltInTunerType(Context context) {
+        if (mBuiltInTunerType == null) {
+            mBuiltInTunerType = 0;
+            if (CustomizationManager.hasLinuxDvbBuiltInTuner(context)
+                    && DvbTunerHal.getNumberOfDevices(context) > 0) {
+                mBuiltInTunerType = Tuner.BUILT_IN_TUNER_TYPE_LINUX_DVB;
+            }
+        }
+        return mBuiltInTunerType;
+    }
+
+    /**
+     * Creates a TunerHal instance.
+     *
+     * @param context context for creating the TunerHal instance
+     * @return the TunerHal instance
+     */
+    @Override
+    @WorkerThread
+    public synchronized Tuner createInstance(Context context) {
+        Tuner tunerHal = null;
+        if (DvbTunerHal.getNumberOfDevices(context) > 0) {
+            if (DEBUG) Log.d(TAG, "Use DvbTunerHal");
+            tunerHal = new DvbTunerHal(context);
+        }
+        return tunerHal != null && tunerHal.openFirstAvailable() ? tunerHal : null;
+    }
+
+    /**
+     * Returns if tuner input service would use built-in tuners instead of USB tuners or network
+     * tuners.
+     */
+    @Override
+    public boolean useBuiltInTuner(Context context) {
+        return getBuiltInTunerType(context) != 0;
+    }
+
+    /** Gets the number of tuner devices currently present. */
+    @Override
+    @WorkerThread
+    public Pair<Integer, Integer> getTunerTypeAndCount(Context context) {
+        if (useBuiltInTuner(context)) {
+            if (getBuiltInTunerType(context) == Tuner.BUILT_IN_TUNER_TYPE_LINUX_DVB) {
+                return new Pair<>(
+                        Tuner.TUNER_TYPE_BUILT_IN, DvbTunerHal.getNumberOfDevices(context));
+            }
+        } else {
+            int usbTunerCount = DvbTunerHal.getNumberOfDevices(context);
+            if (usbTunerCount > 0) {
+                return new Pair<>(Tuner.TUNER_TYPE_USB, usbTunerCount);
+            }
+        }
+        return new Pair<>(null, 0);
+    }
+}
diff --git a/tuner/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java b/tuner/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java
index 8403324..4a1c7c1 100644
--- a/tuner/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java
+++ b/tuner/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java
@@ -26,6 +26,7 @@
 import com.android.tv.tuner.data.Cea708Data.CaptionPenLocation;
 import com.android.tv.tuner.data.Cea708Data.CaptionWindow;
 import com.android.tv.tuner.data.Cea708Data.CaptionWindowAttr;
+import com.android.tv.tuner.data.Cea708Parser;
 import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
 import java.util.ArrayList;
 
diff --git a/tuner/src/com/android/tv/tuner/data/Cea708Data.java b/tuner/src/com/android/tv/tuner/data/Cea708Data.java
index 73a9018..bd1fc9b 100644
--- a/tuner/src/com/android/tv/tuner/data/Cea708Data.java
+++ b/tuner/src/com/android/tv/tuner/data/Cea708Data.java
@@ -18,7 +18,6 @@
 
 import android.graphics.Color;
 import android.support.annotation.NonNull;
-import com.android.tv.tuner.cc.Cea708Parser;
 
 /** Collection of CEA-708 structures. */
 public class Cea708Data {
diff --git a/tuner/src/com/android/tv/tuner/cc/Cea708Parser.java b/tuner/src/com/android/tv/tuner/data/Cea708Parser.java
similarity index 99%
rename from tuner/src/com/android/tv/tuner/cc/Cea708Parser.java
rename to tuner/src/com/android/tv/tuner/data/Cea708Parser.java
index 4e08027..92834b2 100644
--- a/tuner/src/com/android/tv/tuner/cc/Cea708Parser.java
+++ b/tuner/src/com/android/tv/tuner/data/Cea708Parser.java
@@ -14,13 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.tv.tuner.cc;
+package com.android.tv.tuner.data;
 
 import android.os.SystemClock;
 import android.support.annotation.IntDef;
 import android.util.Log;
 import android.util.SparseIntArray;
-import com.android.tv.tuner.data.Cea708Data;
 import com.android.tv.tuner.data.Cea708Data.CaptionColor;
 import com.android.tv.tuner.data.Cea708Data.CaptionEvent;
 import com.android.tv.tuner.data.Cea708Data.CaptionPenAttr;
diff --git a/tuner/src/com/android/tv/tuner/data/PsipData.java b/tuner/src/com/android/tv/tuner/data/PsipData.java
index 239009d..d4af093 100644
--- a/tuner/src/com/android/tv/tuner/data/PsipData.java
+++ b/tuner/src/com/android/tv/tuner/data/PsipData.java
@@ -22,7 +22,6 @@
 import com.android.tv.common.util.StringUtils;
 import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
 import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
-import com.android.tv.tuner.ts.SectionParser;
 import com.android.tv.tuner.util.ConvertUtils;
 import java.util.ArrayList;
 import java.util.HashMap;
diff --git a/tuner/src/com/android/tv/tuner/ts/SectionParser.java b/tuner/src/com/android/tv/tuner/data/SectionParser.java
similarity index 99%
rename from tuner/src/com/android/tv/tuner/ts/SectionParser.java
rename to tuner/src/com/android/tv/tuner/data/SectionParser.java
index 27726c0..d3dba6b 100644
--- a/tuner/src/com/android/tv/tuner/ts/SectionParser.java
+++ b/tuner/src/com/android/tv/tuner/data/SectionParser.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.tv.tuner.ts;
+package com.android.tv.tuner.data;
 
 import android.media.tv.TvContentRating;
 import android.media.tv.TvContract.Programs.Genres;
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java b/tuner/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java
index 1f48c45..5c20330 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java
@@ -17,8 +17,8 @@
 package com.android.tv.tuner.exoplayer;
 
 import android.util.Log;
-import com.android.tv.tuner.cc.Cea708Parser;
 import com.android.tv.tuner.data.Cea708Data.CaptionEvent;
+import com.android.tv.tuner.data.Cea708Parser;
 import com.google.android.exoplayer.ExoPlaybackException;
 import com.google.android.exoplayer.MediaClock;
 import com.google.android.exoplayer.MediaFormat;
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java b/tuner/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
index e10a299..e48cb03 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
@@ -23,13 +23,14 @@
 import android.os.Looper;
 import android.os.Message;
 import android.os.SystemClock;
+import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
 import android.util.Pair;
 import com.android.tv.tuner.exoplayer.audio.MpegTsDefaultAudioTrackRenderer;
 import com.android.tv.tuner.exoplayer.buffer.BufferManager;
+import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener;
 import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer;
 import com.android.tv.tuner.exoplayer.buffer.SimpleSampleBuffer;
-import com.android.tv.tuner.tvinput.PlaybackBufferListener;
 import com.google.android.exoplayer.MediaFormat;
 import com.google.android.exoplayer.MediaFormatHolder;
 import com.google.android.exoplayer.SampleHolder;
@@ -49,6 +50,8 @@
 import com.google.android.exoplayer2.trackselection.TrackSelection;
 import com.google.android.exoplayer2.upstream.DataSpec;
 import com.google.android.exoplayer2.upstream.DefaultAllocator;
+import com.google.android.exoplayer2.upstream.TransferListener;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -69,6 +72,7 @@
     private final long mId;
 
     private final Handler.Callback mSourceReaderWorker;
+    private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
 
     private BufferManager.SampleBuffer mSampleBuffer;
     private Handler mSourceReaderHandler;
@@ -90,7 +94,8 @@
             final DataSource source,
             BufferManager bufferManager,
             PlaybackBufferListener bufferListener,
-            boolean isRecording) {
+            boolean isRecording,
+            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlagsoncurrentDvrPlaybackFlags) {
         this(
                 uri,
                 source,
@@ -98,10 +103,12 @@
                 bufferListener,
                 isRecording,
                 Looper.myLooper(),
-                new HandlerThread("SourceReaderThread"));
+                new HandlerThread("SourceReaderThread"),
+                concurrentDvrPlaybackFlagsoncurrentDvrPlaybackFlags);
     }
 
     @VisibleForTesting
+    @SuppressWarnings("MissingOverride")
     public ExoPlayerSampleExtractor(
             Uri uri,
             DataSource source,
@@ -109,9 +116,11 @@
             PlaybackBufferListener bufferListener,
             boolean isRecording,
             Looper workerLooper,
-            HandlerThread sourceReaderThread) {
+            HandlerThread sourceReaderThread,
+            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) {
         // It'll be used as a timeshift file chunk name's prefix.
         mId = System.currentTimeMillis();
+        mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
 
         EventListener eventListener =
                 new EventListener() {
@@ -134,8 +143,19 @@
                                         // DataSource interface.
                                         return new com.google.android.exoplayer2.upstream
                                                 .DataSource() {
+
+                                            private @Nullable Uri uri;
+
+                                            // TODO: uncomment once this is part of the public API.
+                                            // @Override
+                                            public void addTransferListener(
+                                                    TransferListener transferListener) {
+                                                // Do nothing. Unsupported in V1.
+                                            }
+
                                             @Override
                                             public long open(DataSpec dataSpec) throws IOException {
+                                                this.uri = dataSpec.uri;
                                                 return source.open(
                                                         new com.google.android.exoplayer.upstream
                                                                 .DataSpec(
@@ -156,13 +176,14 @@
                                             }
 
                                             @Override
-                                            public Uri getUri() {
-                                                return null;
+                                            public @Nullable Uri getUri() {
+                                                return uri;
                                             }
 
                                             @Override
                                             public void close() throws IOException {
                                                 source.close();
+                                                uri = null;
                                             }
                                         };
                                     }
@@ -176,6 +197,7 @@
                             bufferManager,
                             bufferListener,
                             false,
+                            mConcurrentDvrPlaybackFlags,
                             RecordingSampleBuffer.BUFFER_REASON_RECORDING);
         } else {
             if (bufferManager == null) {
@@ -186,6 +208,7 @@
                                 bufferManager,
                                 bufferListener,
                                 true,
+                                mConcurrentDvrPlaybackFlags,
                                 RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK);
             }
         }
@@ -204,6 +227,7 @@
         private static final int RETRY_INTERVAL_MS = 50;
 
         private final MediaSource mSampleSource;
+        private final MediaSource.SourceInfoRefreshListener mSampleSourceListener;
         private MediaPeriod mMediaPeriod;
         private SampleStream[] mStreams;
         private boolean[] mTrackMetEos;
@@ -215,17 +239,16 @@
 
         public SourceReaderWorker(MediaSource sampleSource) {
             mSampleSource = sampleSource;
-            mSampleSource.prepareSource(
-                    null,
-                    false,
-                    new MediaSource.Listener() {
+            mSampleSourceListener =
+                    new MediaSource.SourceInfoRefreshListener() {
                         @Override
                         public void onSourceInfoRefreshed(
                                 MediaSource source, Timeline timeline, Object manifest) {
                             // Dynamic stream change is not supported yet. b/28169263
                             // For now, this will cause EOS and playback reset.
                         }
-                    });
+                    };
+            mSampleSource.prepareSource(null, false, mSampleSourceListener, null);
             mDecoderInputBuffer =
                     new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
             mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
@@ -283,11 +306,10 @@
                 // This instance is already released while the extractor is preparing.
                 return;
             }
-            TrackSelection.Factory selectionFactory = new FixedTrackSelection.Factory();
             TrackGroupArray trackGroupArray = mMediaPeriod.getTrackGroups();
             TrackSelection[] selections = new TrackSelection[trackGroupArray.length];
             for (int i = 0; i < selections.length; ++i) {
-                selections[i] = selectionFactory.createTrackSelection(trackGroupArray.get(i), 0);
+                selections[i] = new FixedTrackSelection(trackGroupArray.get(i), 0);
             }
             boolean[] retain = new boolean[trackGroupArray.length];
             boolean[] reset = new boolean[trackGroupArray.length];
@@ -343,7 +365,9 @@
                         mMediaPeriod =
                                 mSampleSource.createPeriod(
                                         new MediaSource.MediaPeriodId(0),
-                                        new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE));
+                                        new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE)
+// AOSP_Comment_Out                                         , 0
+                                );
                         mMediaPeriod.prepare(this, 0);
                         try {
                             mMediaPeriod.maybeThrowPrepareError();
@@ -382,7 +406,7 @@
                 case MSG_RELEASE:
                     if (mMediaPeriod != null) {
                         mSampleSource.releasePeriod(mMediaPeriod);
-                        mSampleSource.releaseSource();
+                        mSampleSource.releaseSource(mSampleSourceListener);
                         mMediaPeriod = null;
                     }
                     cleanUp();
@@ -607,12 +631,7 @@
             final long lastExtractedPositionUs = getLastExtractedPositionUs();
             if (mOnCompletionListenerHandler != null && mOnCompletionListener != null) {
                 mOnCompletionListenerHandler.post(
-                        new Runnable() {
-                            @Override
-                            public void run() {
-                                listener.onCompletion(result, lastExtractedPositionUs);
-                            }
-                        });
+                        () -> listener.onCompletion(result, lastExtractedPositionUs));
             }
         }
     }
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java b/tuner/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java
index e722442..9749e4b 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java
@@ -18,12 +18,13 @@
 
 import android.os.Handler;
 import com.android.tv.tuner.exoplayer.buffer.BufferManager;
+import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener;
 import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer;
-import com.android.tv.tuner.tvinput.PlaybackBufferListener;
 import com.google.android.exoplayer.MediaFormat;
 import com.google.android.exoplayer.MediaFormatHolder;
 import com.google.android.exoplayer.MediaFormatUtil;
 import com.google.android.exoplayer.SampleHolder;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
@@ -43,10 +44,15 @@
     private final BufferManager mBufferManager;
     private final PlaybackBufferListener mBufferListener;
     private BufferManager.SampleBuffer mSampleBuffer;
+    private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
 
-    public FileSampleExtractor(BufferManager bufferManager, PlaybackBufferListener bufferListener) {
+    public FileSampleExtractor(
+            BufferManager bufferManager,
+            PlaybackBufferListener bufferListener,
+            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) {
         mBufferManager = bufferManager;
         mBufferListener = bufferListener;
+        mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
         mTrackCount = -1;
     }
 
@@ -74,6 +80,7 @@
                         mBufferManager,
                         mBufferListener,
                         true,
+                        mConcurrentDvrPlaybackFlags,
                         RecordingSampleBuffer.BUFFER_REASON_RECORDED_PLAYBACK);
         mSampleBuffer.init(ids, mTrackFormats);
         return true;
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
index a49cbfa..6781c61 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
@@ -31,8 +31,8 @@
 import com.android.tv.tuner.exoplayer.audio.MpegTsMediaCodecAudioTrackRenderer;
 import com.android.tv.tuner.source.TsDataSource;
 import com.android.tv.tuner.source.TsDataSourceManager;
-import com.android.tv.tuner.tvinput.EventDetector;
-import com.android.tv.tuner.tvinput.TunerDebug;
+import com.android.tv.tuner.ts.EventDetector.EventListener;
+import com.android.tv.tuner.tvinput.debug.TunerDebug;
 import com.google.android.exoplayer.DummyTrackRenderer;
 import com.google.android.exoplayer.ExoPlaybackException;
 import com.google.android.exoplayer.ExoPlayer;
@@ -58,10 +58,7 @@
     /** Interface definition for building specific track renderers. */
     public interface RendererBuilder {
         void buildRenderers(
-                MpegTsPlayer mpegTsPlayer,
-                DataSource dataSource,
-                boolean hasSoftwareAudioDecoder,
-                RendererBuilderCallback callback);
+                MpegTsPlayer mpegTsPlayer, DataSource dataSource, RendererBuilderCallback callback);
     }
 
     /** Interface definition for {@link RendererBuilder#buildRenderers} to notify the result. */
@@ -229,7 +226,7 @@
             Context context,
             TunerChannel channel,
             boolean hasSoftwareAudioDecoder,
-            EventDetector.EventListener eventListener) {
+            EventListener eventListener) {
         TsDataSource source = null;
         if (channel != null) {
             source = mSourceManager.createDataSource(context, channel, eventListener);
@@ -246,7 +243,7 @@
         }
         mRendererBuildingState = RENDERER_BUILDING_STATE_BUILDING;
         mBuilderCallback = new InternalRendererBuilderCallback();
-        mRendererBuilder.buildRenderers(this, source, hasSoftwareAudioDecoder, mBuilderCallback);
+        mRendererBuilder.buildRenderers(this, source, mBuilderCallback);
         return true;
     }
 
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java
index 774285e..e043907 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java
@@ -17,41 +17,48 @@
 package com.android.tv.tuner.exoplayer;
 
 import android.content.Context;
-import com.android.tv.tuner.TunerFeatures;
 import com.android.tv.tuner.exoplayer.MpegTsPlayer.RendererBuilder;
 import com.android.tv.tuner.exoplayer.MpegTsPlayer.RendererBuilderCallback;
 import com.android.tv.tuner.exoplayer.audio.MpegTsDefaultAudioTrackRenderer;
 import com.android.tv.tuner.exoplayer.buffer.BufferManager;
-import com.android.tv.tuner.tvinput.PlaybackBufferListener;
+import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener;
 import com.google.android.exoplayer.MediaCodecSelector;
 import com.google.android.exoplayer.SampleSource;
 import com.google.android.exoplayer.TrackRenderer;
 import com.google.android.exoplayer.upstream.DataSource;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
 
 /** Builder for renderer objects for {@link MpegTsPlayer}. */
 public class MpegTsRendererBuilder implements RendererBuilder {
     private final Context mContext;
     private final BufferManager mBufferManager;
     private final PlaybackBufferListener mBufferListener;
+    private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
 
     public MpegTsRendererBuilder(
-            Context context, BufferManager bufferManager, PlaybackBufferListener bufferListener) {
+            Context context,
+            BufferManager bufferManager,
+            PlaybackBufferListener bufferListener,
+            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) {
         mContext = context;
         mBufferManager = bufferManager;
         mBufferListener = bufferListener;
+        mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
     }
 
     @Override
     public void buildRenderers(
-            MpegTsPlayer mpegTsPlayer,
-            DataSource dataSource,
-            boolean mHasSoftwareAudioDecoder,
-            RendererBuilderCallback callback) {
+            MpegTsPlayer mpegTsPlayer, DataSource dataSource, RendererBuilderCallback callback) {
         // Build the video and audio renderers.
         SampleExtractor extractor =
                 dataSource == null
-                        ? new MpegTsSampleExtractor(mBufferManager, mBufferListener)
-                        : new MpegTsSampleExtractor(dataSource, mBufferManager, mBufferListener);
+                        ? new MpegTsSampleExtractor(
+                                mBufferManager, mBufferListener, mConcurrentDvrPlaybackFlags)
+                        : new MpegTsSampleExtractor(
+                                dataSource,
+                                mBufferManager,
+                                mBufferListener,
+                                mConcurrentDvrPlaybackFlags);
         SampleSource sampleSource = new MpegTsSampleSource(extractor);
         MpegTsVideoTrackRenderer videoRenderer =
                 new MpegTsVideoTrackRenderer(
@@ -63,9 +70,7 @@
                         sampleSource,
                         MediaCodecSelector.DEFAULT,
                         mpegTsPlayer.getMainHandler(),
-                        mpegTsPlayer,
-                        mHasSoftwareAudioDecoder,
-                        !TunerFeatures.AC3_SOFTWARE_DECODE.isEnabled(mContext));
+                        mpegTsPlayer);
         Cea708TextTrackRenderer textRenderer = new Cea708TextTrackRenderer(sampleSource);
 
         TrackRenderer[] renderers = new TrackRenderer[MpegTsPlayer.RENDERER_COUNT];
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java
index 593b576..582f18c 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java
@@ -19,14 +19,15 @@
 import android.net.Uri;
 import android.os.Handler;
 import com.android.tv.tuner.exoplayer.buffer.BufferManager;
+import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener;
 import com.android.tv.tuner.exoplayer.buffer.SamplePool;
-import com.android.tv.tuner.tvinput.PlaybackBufferListener;
 import com.google.android.exoplayer.MediaFormat;
 import com.google.android.exoplayer.MediaFormatHolder;
 import com.google.android.exoplayer.SampleHolder;
 import com.google.android.exoplayer.SampleSource;
 import com.google.android.exoplayer.upstream.DataSource;
 import com.google.android.exoplayer.util.MimeTypes;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
@@ -63,13 +64,22 @@
      * @param source the {@link DataSource} to extract from
      * @param bufferManager the manager for reading & writing samples backed by physical storage
      * @param bufferListener the {@link PlaybackBufferListener} to notify buffer storage status
-     *     change
+     * @param concurrentDvrPlaybackFlags
      */
     public MpegTsSampleExtractor(
-            DataSource source, BufferManager bufferManager, PlaybackBufferListener bufferListener) {
+            DataSource source,
+            BufferManager bufferManager,
+            PlaybackBufferListener bufferListener,
+            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) {
+
         mSampleExtractor =
                 new ExoPlayerSampleExtractor(
-                        Uri.EMPTY, source, bufferManager, bufferListener, false);
+                        Uri.EMPTY,
+                        source,
+                        bufferManager,
+                        bufferListener,
+                        false,
+                        concurrentDvrPlaybackFlags);
         init();
     }
 
@@ -81,8 +91,11 @@
      *     change
      */
     public MpegTsSampleExtractor(
-            BufferManager bufferManager, PlaybackBufferListener bufferListener) {
-        mSampleExtractor = new FileSampleExtractor(bufferManager, bufferListener);
+            BufferManager bufferManager,
+            PlaybackBufferListener bufferListener,
+            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) {
+        mSampleExtractor =
+                new FileSampleExtractor(bufferManager, bufferListener, concurrentDvrPlaybackFlags);
         init();
     }
 
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsVideoTrackRenderer.java b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsVideoTrackRenderer.java
index b136e23..c8a9c01 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsVideoTrackRenderer.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsVideoTrackRenderer.java
@@ -19,7 +19,7 @@
 import android.media.MediaCodec;
 import android.os.Handler;
 import android.util.Log;
-import com.android.tv.tuner.TunerFeatures;
+import com.android.tv.tuner.features.TunerFeatures;
 import com.google.android.exoplayer.DecoderInfo;
 import com.google.android.exoplayer.ExoPlaybackException;
 import com.google.android.exoplayer.MediaCodecSelector;
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java b/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java
index 944cfbc..bab74c9 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java
@@ -21,7 +21,7 @@
 import android.os.Handler;
 import android.os.SystemClock;
 import android.util.Log;
-import com.android.tv.tuner.tvinput.TunerDebug;
+import com.android.tv.tuner.tvinput.debug.TunerDebug;
 import com.google.android.exoplayer.CodecCounters;
 import com.google.android.exoplayer.ExoPlaybackException;
 import com.google.android.exoplayer.MediaClock;
@@ -106,8 +106,6 @@
     private final Handler mEventHandler;
     private final AudioTrackMonitor mMonitor;
     private final AudioClock mAudioClock;
-    private final boolean mAc3Passthrough;
-    private final boolean mSoftwareDecoderAvailable;
 
     private MediaFormat mFormat;
     private SampleHolder mSampleHolder;
@@ -137,9 +135,7 @@
             SampleSource source,
             MediaCodecSelector selector,
             Handler eventHandler,
-            EventListener listener,
-            boolean hasSoftwareAudioDecoder,
-            boolean usePassthrough) {
+            EventListener listener) {
         mSource = source.register();
         mSelector = selector;
         mEventHandler = eventHandler;
@@ -152,9 +148,6 @@
         mMonitor = new AudioTrackMonitor();
         mAudioClock = new AudioClock();
         mTracksIndex = new ArrayList<>();
-        mAc3Passthrough = usePassthrough;
-        // TODO reimplement ffmpeg decoder check for google3
-        mSoftwareDecoderAvailable = false;
     }
 
     @Override
@@ -379,19 +372,6 @@
         }
     }
 
-    private MediaFormat convertMediaFormatToRaw(MediaFormat format) {
-        return MediaFormat.createAudioFormat(
-                format.trackId,
-                MimeTypes.AUDIO_RAW,
-                format.bitrate,
-                format.maxInputSize,
-                format.durationUs,
-                format.channelCount,
-                format.sampleRate,
-                format.initializationData,
-                format.language);
-    }
-
     private void onInputFormatChanged(MediaFormatHolder formatHolder) throws ExoPlaybackException {
         String mimeType = formatHolder.format.mimeType;
         mUseFrameworkDecoder = MediaCodecAudioDecoder.supportMimeType(mSelector, mimeType);
@@ -662,26 +642,14 @@
         if (mEventHandler == null || mEventListener == null) {
             return;
         }
-        mEventHandler.post(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        mEventListener.onAudioTrackInitializationError(e);
-                    }
-                });
+        mEventHandler.post(() -> mEventListener.onAudioTrackInitializationError(e));
     }
 
     private void notifyAudioTrackWriteError(final AudioTrack.WriteException e) {
         if (mEventHandler == null || mEventListener == null) {
             return;
         }
-        mEventHandler.post(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        mEventListener.onAudioTrackWriteError(e);
-                    }
-                });
+        mEventHandler.post(() -> mEventListener.onAudioTrackWriteError(e));
     }
 
     @Override
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java b/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java
index b382545..c655f77 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java
@@ -69,13 +69,7 @@
 
     private void notifyAudioTrackSetPlaybackParamsError(final IllegalArgumentException e) {
         if (eventHandler != null && mListener != null) {
-            eventHandler.post(
-                    new Runnable() {
-                        @Override
-                        public void run() {
-                            mListener.onAudioTrackSetPlaybackParamsError(e);
-                        }
-                    });
+            eventHandler.post(() -> mListener.onAudioTrackSetPlaybackParamsError(e));
         }
     }
 
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
index 3e4ab10..c32540c 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
@@ -284,6 +284,20 @@
          */
         void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index)
                 throws IOException;
+
+        /**
+         * Writes to index file to storage.
+         *
+         * @param trackName track name
+         * @param size size of sample
+         * @param position position in micro seconds
+         * @param sampleChunk {@link SampleChunk} chunk to be added
+         * @param offset offset
+         * @throws IOException
+         */
+        void updateIndexFile(
+                String trackName, int size, long position, SampleChunk sampleChunk, int offset)
+                throws IOException;
     }
 
     private static class EvictChunkQueueMap {
@@ -368,7 +382,8 @@
             long positionUs,
             SamplePool samplePool,
             SampleChunk currentChunk,
-            int currentOffset)
+            int currentOffset,
+            boolean updateIndexFile)
             throws IOException {
         if (!maybeEvictChunk()) {
             throw new IOException("Not enough storage space");
@@ -386,9 +401,16 @@
                     mSampleChunkCreator.createSampleChunk(
                             samplePool, file, positionUs, mChunkCallback);
             map.put(positionUs, new Pair(sampleChunk, 0));
+            if (updateIndexFile) {
+                mStorageManager.updateIndexFile(id, map.size(), positionUs, sampleChunk, 0);
+            }
             return sampleChunk;
         } else {
             map.put(positionUs, new Pair(currentChunk, currentOffset));
+            if (updateIndexFile) {
+                mStorageManager.updateIndexFile(
+                        id, map.size(), positionUs, currentChunk, currentOffset);
+            }
             return null;
         }
     }
@@ -587,6 +609,26 @@
         }
     }
 
+    /**
+     * Writes track information for all tracks.
+     *
+     * @param audios list of audio track information
+     * @param videos list of audio track information
+     * @throws IOException
+     */
+    public void writeMetaFilesOnly(List<TrackFormat> audios, List<TrackFormat> videos)
+            throws IOException {
+        if (audios.isEmpty() && videos.isEmpty()) {
+            throw new IOException("No track information to save");
+        }
+        if (!audios.isEmpty()) {
+            mStorageManager.writeTrackInfoFiles(audios, true);
+        }
+        if (!videos.isEmpty()) {
+            mStorageManager.writeTrackInfoFiles(videos, false);
+        }
+    }
+
     /** Releases all the resources. */
     public void release() {
         try {
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
index 2a58ffc..f19756e 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
@@ -27,6 +27,7 @@
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.RandomAccessFile;
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
@@ -388,4 +389,22 @@
             }
         }
     }
+
+    @Override
+    public void updateIndexFile(
+            String trackName, int size, long position, SampleChunk sampleChunk, int offset)
+            throws IOException {
+        File indexFile = new File(getBufferDir(), trackName + IDX_FILE_SUFFIX_V2);
+        if (!indexFile.exists()) {
+            indexFile.createNewFile();
+        }
+        RandomAccessFile accessFile = new RandomAccessFile(indexFile, "rw");
+        accessFile.seek(0);
+        accessFile.writeLong(size);
+        accessFile.seek(accessFile.length());
+        accessFile.writeLong(position);
+        accessFile.writeLong(sampleChunk.getStartPositionUs());
+        accessFile.writeInt(offset);
+        accessFile.close();
+    }
 }
diff --git a/tuner/src/com/android/tv/tuner/tvinput/PlaybackBufferListener.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/PlaybackBufferListener.java
similarity index 96%
rename from tuner/src/com/android/tv/tuner/tvinput/PlaybackBufferListener.java
rename to tuner/src/com/android/tv/tuner/exoplayer/buffer/PlaybackBufferListener.java
index 1628bcf..046cfbe 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/PlaybackBufferListener.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/PlaybackBufferListener.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.tv.tuner.tvinput;
+package com.android.tv.tuner.exoplayer.buffer;
 
 /** The listener for buffer events occurred during playback. */
 public interface PlaybackBufferListener {
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
index ebf00f5..d95642c 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
@@ -22,12 +22,12 @@
 import android.util.Log;
 import com.android.tv.tuner.exoplayer.MpegTsPlayer;
 import com.android.tv.tuner.exoplayer.SampleExtractor;
-import com.android.tv.tuner.tvinput.PlaybackBufferListener;
 import com.google.android.exoplayer.C;
 import com.google.android.exoplayer.MediaFormat;
 import com.google.android.exoplayer.SampleHolder;
 import com.google.android.exoplayer.SampleSource;
 import com.google.android.exoplayer.util.Assertions;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
 import java.io.IOException;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -69,6 +69,7 @@
     private final BufferManager mBufferManager;
     private final PlaybackBufferListener mBufferListener;
     private final @BufferReason int mBufferReason;
+    private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
 
     private int mTrackCount;
     private boolean[] mTrackSelected;
@@ -103,15 +104,18 @@
      * @param bufferManager the manager of {@link SampleChunk}
      * @param bufferListener the listener for buffer I/O event
      * @param enableTrickplay {@code true} when trickplay should be enabled
-     * @param bufferReason the reason for caching samples {@link RecordingSampleBuffer.BufferReason}
+     * @param concurrentDvrPlaybackFlags
+     * @param bufferReason the reason for caching samples {@link BufferReason}
      */
     public RecordingSampleBuffer(
             BufferManager bufferManager,
             PlaybackBufferListener bufferListener,
             boolean enableTrickplay,
+            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
             @BufferReason int bufferReason) {
         mBufferManager = bufferManager;
         mBufferListener = bufferListener;
+        mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
         if (bufferListener != null) {
             bufferListener.onBufferStateChanged(enableTrickplay);
         }
@@ -129,7 +133,13 @@
         mReadSampleQueues = new ArrayList<>();
         mSampleChunkIoHelper =
                 new SampleChunkIoHelper(
-                        ids, mediaFormats, mBufferReason, mBufferManager, mSamplePool, mIoCallback);
+                        ids,
+                        mediaFormats,
+                        mBufferReason,
+                        mBufferManager,
+                        mSamplePool,
+                        mIoCallback,
+                        mConcurrentDvrPlaybackFlags);
         for (int i = 0; i < mTrackCount; ++i) {
             mReadSampleQueues.add(i, new SampleQueue(mSamplePool));
         }
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
index d95d0ad..f4d3bf8 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
@@ -29,7 +29,9 @@
 import com.google.android.exoplayer.MediaFormat;
 import com.google.android.exoplayer.SampleHolder;
 import com.google.android.exoplayer.util.MimeTypes;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
@@ -52,6 +54,7 @@
     private static final int MSG_READ = 5;
     private static final int MSG_WRITE = 6;
     private static final int MSG_RELEASE = 7;
+    private static final int MSG_UPDATE_INDEX = 8;
 
     private final long mSampleChunkDurationUs;
     private final int mTrackCount;
@@ -61,6 +64,7 @@
     private final BufferManager mBufferManager;
     private final SamplePool mSamplePool;
     private final IoCallback mIoCallback;
+    private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
 
     private Handler mIoHandler;
     private final ConcurrentLinkedQueue<SampleHolder> mReadSampleBuffers[];
@@ -70,6 +74,8 @@
     private final SampleChunk.IoState[] mReadIoStates;
     private final SampleChunk.IoState[] mWriteIoStates;
     private final Set<Integer> mSelectedTracks = new ArraySet<>();
+    private final long[] mReadChunkOffset;
+    private final long[] mReadChunkPositionUs;
     private long mBufferDurationUs = 0;
     private boolean mWriteEnded;
     private boolean mErrorNotified;
@@ -115,6 +121,7 @@
      * @param bufferManager manager of {@link SampleChunk} collections
      * @param samplePool allocator for a sample
      * @param ioCallback listeners for I/O events
+     * @param concurrentDvrPlaybackFlags
      */
     public SampleChunkIoHelper(
             List<String> ids,
@@ -122,7 +129,8 @@
             @BufferReason int bufferReason,
             BufferManager bufferManager,
             SamplePool samplePool,
-            IoCallback ioCallback) {
+            IoCallback ioCallback,
+            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) {
         mTrackCount = ids.size();
         mIds = ids;
         mMediaFormats = mediaFormats;
@@ -130,11 +138,14 @@
         mBufferManager = bufferManager;
         mSamplePool = samplePool;
         mIoCallback = ioCallback;
+        mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
 
         mReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount];
         mHandlerReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount];
         mWriteIndexEndPositionUs = new long[mTrackCount];
         mWriteChunkEndPositionUs = new long[mTrackCount];
+        mReadChunkOffset = new long[mTrackCount];
+        mReadChunkPositionUs = new long[mTrackCount];
         mReadIoStates = new SampleChunk.IoState[mTrackCount];
         mWriteIoStates = new SampleChunk.IoState[mTrackCount];
 
@@ -171,6 +182,29 @@
                 mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_OPEN_WRITE, i));
             }
         }
+
+        try {
+            if (mConcurrentDvrPlaybackFlags.enabled()
+                    && mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDING
+                    && mTrackCount > 0) {
+                // Saves meta information for recording.
+                List<BufferManager.TrackFormat> audios = new ArrayList<>(mTrackCount);
+                List<BufferManager.TrackFormat> videos = new ArrayList<>(mTrackCount);
+                for (int i = 0; i < mTrackCount; ++i) {
+                    android.media.MediaFormat format =
+                            mMediaFormats.get(i).getFrameworkMediaFormatV16();
+                    format.setLong(android.media.MediaFormat.KEY_DURATION, mBufferDurationUs);
+                    if (MimeTypes.isAudio(mMediaFormats.get(i).mimeType)) {
+                        audios.add(new BufferManager.TrackFormat(mIds.get(i), format));
+                    } else if (MimeTypes.isVideo(mMediaFormats.get(i).mimeType)) {
+                        videos.add(new BufferManager.TrackFormat(mIds.get(i), format));
+                    }
+                }
+                mBufferManager.writeMetaFilesOnly(audios, videos);
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Unable to write Meta files for DVR recording.", e);
+        }
     }
 
     /**
@@ -217,6 +251,18 @@
     }
 
     /**
+     * Update Index from the specified offset.
+     *
+     * @param index track index
+     * @param offset of the specified position
+     */
+    private void updateIndex(int index, long offset) {
+        IoParams params =
+                new IoParams(index, offset, null, null, null); // mReadSampleBuffers[index]);
+        mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_UPDATE_INDEX, params));
+    }
+
+    /**
      * Closes read from the specified track.
      *
      * @param index track index
@@ -300,6 +346,9 @@
                 case MSG_RELEASE:
                     doRelease((ConditionVariable) message.obj);
                     return true;
+                case MSG_UPDATE_INDEX:
+                    doUpdateIndex((IoParams) message.obj);
+                    return true;
             }
         } catch (IOException e) {
             mIoCallback.onIoError();
@@ -334,8 +383,15 @@
     }
 
     private void doOpenWrite(int index) throws IOException {
+        boolean updateIndexFile =
+                mConcurrentDvrPlaybackFlags.enabled()
+                        && (mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDING)
+                        && (MimeTypes.isVideo(mMediaFormats.get(index).mimeType)
+                                || MimeTypes.isAudio(mMediaFormats.get(index).mimeType));
+
         SampleChunk chunk =
-                mBufferManager.createNewWriteFileIfNeeded(mIds.get(index), 0, mSamplePool, null, 0);
+                mBufferManager.createNewWriteFileIfNeeded(
+                        mIds.get(index), 0, mSamplePool, null, 0, updateIndexFile);
         mWriteIoStates[index].openWrite(chunk);
     }
 
@@ -370,7 +426,16 @@
             SampleHolder sample = mReadIoStates[index].read();
             if (sample != null) {
                 mHandlerReadSampleBuffers[index].offer(sample);
+                if (mConcurrentDvrPlaybackFlags.enabled()) {
+                    mReadChunkOffset[index] = mReadIoStates[index].getOffset();
+                    mReadChunkPositionUs[index] = sample.timeUs;
+                }
             } else {
+                if (mConcurrentDvrPlaybackFlags.enabled()
+                        && mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDED_PLAYBACK) {
+                    // Update Index, to load new Samples
+                    updateIndex(index, mReadChunkOffset[index]);
+                }
                 // Read reached write but write is not finished yet --- wait a few moments to
                 // see if another sample is written.
                 mIoHandler.sendMessageDelayed(
@@ -379,6 +444,27 @@
         }
     }
 
+    public void doUpdateIndex(IoParams params) throws IOException {
+        int index = params.index;
+        mIoHandler.removeMessages(MSG_READ, index);
+        // Update Track from Storage to load new Samples
+        mBufferManager.loadTrackFromStorage(mIds.get(index), mSamplePool);
+        Pair<SampleChunk, Integer> readPosition =
+                mBufferManager.getReadFile(mIds.get(index), mReadChunkPositionUs[index]);
+        if (readPosition == null) {
+            String errorMessage =
+                    "Chunk ID:"
+                            + mIds.get(index)
+                            + " pos:"
+                            + mReadChunkPositionUs[index]
+                            + "is not found";
+            SoftPreconditions.checkNotNull(readPosition, TAG, errorMessage);
+            throw new IOException(errorMessage);
+        }
+        mReadIoStates[index].openRead(readPosition.first, params.positionUs);
+        mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_READ, index));
+    }
+
     private void doWrite(IoParams params) throws IOException {
         try {
             if (mWriteEnded) {
@@ -398,13 +484,22 @@
                                     ? null
                                     : mWriteIoStates[params.index].getChunk();
                     int currentOffset = (int) mWriteIoStates[params.index].getOffset();
+                    boolean updateIndexFile =
+                            mConcurrentDvrPlaybackFlags.enabled()
+                                    && (mBufferReason
+                                            == RecordingSampleBuffer.BUFFER_REASON_RECORDING)
+                                    && (MimeTypes.isVideo(mMediaFormats.get(index).mimeType)
+                                            || MimeTypes.isAudio(
+                                                    mMediaFormats.get(index).mimeType));
+
                     nextChunk =
                             mBufferManager.createNewWriteFileIfNeeded(
                                     mIds.get(index),
                                     mWriteIndexEndPositionUs[index],
                                     mSamplePool,
                                     currentChunk,
-                                    currentOffset);
+                                    currentOffset,
+                                    updateIndexFile);
                     mWriteIndexEndPositionUs[index] =
                             ((sample.timeUs / RecordingSampleBuffer.MIN_SEEK_DURATION_US) + 1)
                                     * RecordingSampleBuffer.MIN_SEEK_DURATION_US;
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java
index 4c6260b..843df7d 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java
@@ -20,7 +20,6 @@
 import android.support.annotation.NonNull;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.tuner.exoplayer.SampleExtractor;
-import com.android.tv.tuner.tvinput.PlaybackBufferListener;
 import com.google.android.exoplayer.C;
 import com.google.android.exoplayer.MediaFormat;
 import com.google.android.exoplayer.SampleHolder;
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java
index b22b8af..3721706 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java
@@ -142,4 +142,8 @@
     @Override
     public void writeIndexFile(
             String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index) {}
+
+    @Override
+    public void updateIndexFile(
+            String trackName, int size, long position, SampleChunk sampleChunk, int offset) {}
 }
diff --git a/tuner/src/com/android/tv/tuner/exoplayer2/VideoRendererExoV2.java b/tuner/src/com/android/tv/tuner/exoplayer2/VideoRendererExoV2.java
new file mode 100644
index 0000000..1203900
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/exoplayer2/VideoRendererExoV2.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.exoplayer2;
+
+import android.content.Context;
+import android.os.Handler;
+import android.util.Log;
+import com.android.tv.tuner.features.TunerFeatures;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
+import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
+import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
+import com.google.android.exoplayer2.video.VideoRendererEventListener;
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Subclasses {@link MediaCodecVideoRenderer} to customize minor behaviors.
+ *
+ * <p>This class changes two behaviors from {@link MediaCodecVideoRenderer}:
+ *
+ * <ul>
+ *   <li>Prefer software decoders for sub-HD streams.
+ *   <li>Prevents the rendering of the first frame when audio can start playing before the first
+ *       video key frame's presentation timestamp.
+ * </ul>
+ */
+public class VideoRendererExoV2 extends MediaCodecVideoRenderer {
+    private static final String TAG = "MpegTsVideoTrackRender";
+
+    private static final String SOFTWARE_DECODER_NAME_PREFIX = "OMX.google.";
+    private static final long ALLOWED_JOINING_TIME_MS = 5000;
+    private static final int DROPPED_FRAMES_NOTIFICATION_THRESHOLD = 10;
+    private static final int MIN_HD_HEIGHT = 720;
+    private static Field sRenderedFirstFrameField;
+
+    private final boolean mIsSwCodecEnabled;
+    private boolean mCodecIsSwPreferred;
+    private boolean mSetRenderedFirstFrame;
+
+    static {
+        // Remove the reflection below once b/31223646 is resolved.
+        try {
+            // TODO: Remove this workaround by using public notification mechanisms.
+            sRenderedFirstFrameField =
+                    MediaCodecVideoRenderer.class.getDeclaredField("renderedFirstFrame");
+            sRenderedFirstFrameField.setAccessible(true);
+        } catch (NoSuchFieldException e) {
+            // Null-checking for {@code sRenderedFirstFrameField} will do the error handling.
+        }
+    }
+
+    /**
+     * Creates an instance.
+     *
+     * @param context A context.
+     * @param handler The handler to use when delivering events to {@code eventListener}. May be
+     *     null if delivery of events is not required.
+     * @param listener The listener of events. May be null if delivery of events is not required.
+     */
+    public VideoRendererExoV2(
+            Context context, Handler handler, VideoRendererEventListener listener) {
+        super(
+                context,
+                MediaCodecSelector.DEFAULT,
+                ALLOWED_JOINING_TIME_MS,
+                handler,
+                listener,
+                DROPPED_FRAMES_NOTIFICATION_THRESHOLD);
+        mIsSwCodecEnabled = TunerFeatures.USE_SW_CODEC_FOR_SD.isEnabled(context);
+    }
+
+    @Override
+    protected List<MediaCodecInfo> getDecoderInfos(
+            MediaCodecSelector codecSelector, Format format, boolean requiresSecureDecoder)
+            throws DecoderQueryException {
+        List<MediaCodecInfo> decoderInfos =
+                super.getDecoderInfos(codecSelector, format, requiresSecureDecoder);
+        if (mIsSwCodecEnabled && mCodecIsSwPreferred) {
+            // If software decoders are preferred, we sort the returned list so that software
+            // decoders appear first.
+            Collections.sort(
+                    decoderInfos,
+                    (o1, o2) ->
+                            // Negate the result to consider software decoders as lower in
+                            // comparisons.
+                            -Boolean.compare(
+                                    o1.name.startsWith(SOFTWARE_DECODER_NAME_PREFIX),
+                                    o2.name.startsWith(SOFTWARE_DECODER_NAME_PREFIX)));
+        }
+        return decoderInfos;
+    }
+
+    @Override
+    protected void onInputFormatChanged(Format format) throws ExoPlaybackException {
+        mCodecIsSwPreferred =
+                MimeTypes.VIDEO_MPEG2.equals(format.sampleMimeType)
+                        && format.height < MIN_HD_HEIGHT;
+        super.onInputFormatChanged(format);
+    }
+
+    @Override
+    protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
+        super.onPositionReset(positionUs, joining);
+        // Disabling pre-rendering of the first frame in order to avoid a frozen picture when
+        // starting the playback. We do this only once, when the renderer is enabled at first, since
+        // we need to pre-render the frame in advance when we do trickplay backed by seeking.
+        if (!mSetRenderedFirstFrame) {
+            setRenderedFirstFrame(true);
+            mSetRenderedFirstFrame = true;
+        }
+    }
+
+    private void setRenderedFirstFrame(boolean renderedFirstFrame) {
+        if (sRenderedFirstFrameField != null) {
+            try {
+                sRenderedFirstFrameField.setBoolean(this, renderedFirstFrame);
+            } catch (IllegalAccessException e) {
+                Log.w(
+                        TAG,
+                        "renderedFirstFrame is not accessible. Playback may start with a frozen"
+                                + " picture.");
+            }
+        }
+    }
+}
diff --git a/tuner/src/com/android/tv/tuner/features/TunerFeatures.java b/tuner/src/com/android/tv/tuner/features/TunerFeatures.java
new file mode 100644
index 0000000..6033a3a
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/features/TunerFeatures.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.tuner.features;
+
+import static com.android.tv.common.feature.FeatureUtils.OFF;
+
+import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.feature.Feature;
+import com.android.tv.common.feature.Model;
+import com.android.tv.common.feature.PropertyFeature;
+import com.android.tv.common.feature.Sdk;
+
+/**
+ * List of {@link Feature} for Tuner.
+ *
+ * <p>Only for use in Tuners.
+ *
+ * <p>Remove the {@code Feature} once it is launched.
+ */
+public class TunerFeatures extends CommonFeatures {
+
+    /**
+     * USE_SW_CODEC_FOR_SD
+     *
+     * <p>Prefer software based codec for SD channels.
+     */
+    public static final Feature USE_SW_CODEC_FOR_SD =
+            PropertyFeature.create(
+                    "use_sw_codec_for_sd",
+                    false
+                    );
+
+    /**
+     * Does the TvProvider on the installed device allow systems inserts to the programs table.
+     *
+     * <p>This is available in {@link Sdk#AT_LEAST_O} but vendors may choose to backport support to
+     * the TvProvider.
+     */
+    public static final Feature TVPROVIDER_ALLOWS_COLUMN_CREATION = Sdk.AT_LEAST_O;
+
+    /** Enable Dvb parsers and listeners. */
+    public static final Feature ENABLE_FILE_DVB = OFF;
+
+    private TunerFeatures() {}
+}
diff --git a/tuner/src/com/android/tv/tuner/layout/ScaledLayout.java b/tuner/src/com/android/tv/tuner/layout/ScaledLayout.java
index dd92b64..6d17be9 100644
--- a/tuner/src/com/android/tv/tuner/layout/ScaledLayout.java
+++ b/tuner/src/com/android/tv/tuner/layout/ScaledLayout.java
@@ -35,14 +35,11 @@
     private static final String TAG = "ScaledLayout";
     private static final boolean DEBUG = false;
     private static final Comparator<Rect> mRectTopLeftSorter =
-            new Comparator<Rect>() {
-                @Override
-                public int compare(Rect lhs, Rect rhs) {
-                    if (lhs.top != rhs.top) {
-                        return lhs.top - rhs.top;
-                    } else {
-                        return lhs.left - rhs.left;
-                    }
+            (Rect lhs, Rect rhs) -> {
+                if (lhs.top != rhs.top) {
+                    return lhs.top - rhs.top;
+                } else {
+                    return lhs.left - rhs.left;
                 }
             };
 
diff --git a/tuner/src/com/android/tv/tuner/livetuner/LiveTvTunerTvInputService.java b/tuner/src/com/android/tv/tuner/livetuner/LiveTvTunerTvInputService.java
index f741fdb..92701db 100644
--- a/tuner/src/com/android/tv/tuner/livetuner/LiveTvTunerTvInputService.java
+++ b/tuner/src/com/android/tv/tuner/livetuner/LiveTvTunerTvInputService.java
@@ -17,6 +17,18 @@
 package com.android.tv.tuner.livetuner;
 
 import com.android.tv.tuner.tvinput.BaseTunerTvInputService;
+import dagger.android.ContributesAndroidInjector;
 
 /** Live TV embedded tuner. */
-public class LiveTvTunerTvInputService extends BaseTunerTvInputService {}
+public class LiveTvTunerTvInputService extends BaseTunerTvInputService {
+
+    /**
+     * Exports {@link LiveTvTunerTvInputService} for Dagger codegen to create the appropriate
+     * injector.
+     */
+    @dagger.Module
+    public abstract static class Module {
+        @ContributesAndroidInjector
+        abstract LiveTvTunerTvInputService contributesLiveTvTunerTvInputServiceInjector();
+    }
+}
diff --git a/src/com/android/tv/util/Filter.java b/tuner/src/com/android/tv/tuner/modules/TunerModule.java
similarity index 64%
copy from src/com/android/tv/util/Filter.java
copy to tuner/src/com/android/tv/tuner/modules/TunerModule.java
index 3e24a49..4843f38 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/tuner/src/com/android/tv/tuner/modules/TunerModule.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,11 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package com.android.tv.tuner.modules;
 
-package com.android.tv.util;
+import com.android.tv.tuner.source.TunerSourceModule;
+import dagger.Module;
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
-}
+/** Dagger module for TV Tuners. */
+@Module(includes = {TunerSingletonsModule.class, TunerSourceModule.class})
+public class TunerModule {}
diff --git a/tuner/src/com/android/tv/tuner/modules/TunerSingletonsModule.java b/tuner/src/com/android/tv/tuner/modules/TunerSingletonsModule.java
new file mode 100644
index 0000000..b7fba8d
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/modules/TunerSingletonsModule.java
@@ -0,0 +1,18 @@
+package com.android.tv.tuner.modules;
+
+import com.android.tv.tuner.singletons.TunerSingletons;
+import dagger.Module;
+
+/**
+ * Provides bindings for items provided by {@link TunerSingletons}.
+ *
+ * <p>Use this module to inject items directly instead of using {@code TunerSingletons}.
+ */
+@Module
+public class TunerSingletonsModule {
+    private final TunerSingletons mTunerSingletons;
+
+    public TunerSingletonsModule(TunerSingletons tunerSingletons) {
+        this.mTunerSingletons = tunerSingletons;
+    }
+}
diff --git a/tuner/src/com/android/tv/tuner/TunerPreferences.java b/tuner/src/com/android/tv/tuner/prefs/TunerPreferences.java
similarity index 98%
rename from tuner/src/com/android/tv/tuner/TunerPreferences.java
rename to tuner/src/com/android/tv/tuner/prefs/TunerPreferences.java
index 7b45b99..85e3a5e 100644
--- a/tuner/src/com/android/tv/tuner/TunerPreferences.java
+++ b/tuner/src/com/android/tv/tuner/prefs/TunerPreferences.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.tv.tuner;
+package com.android.tv.tuner.prefs;
 
 import android.content.Context;
 import android.content.SharedPreferences;
diff --git a/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java b/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java
index 1be4e1c..44f689b 100644
--- a/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java
+++ b/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java
@@ -22,6 +22,7 @@
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
@@ -30,16 +31,13 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.support.annotation.MainThread;
-import android.support.annotation.NonNull;
 import android.support.annotation.VisibleForTesting;
 import android.support.annotation.WorkerThread;
 import android.support.v4.app.NotificationCompat;
 import android.text.TextUtils;
 import android.util.Log;
 import android.widget.Toast;
-import com.android.tv.common.BaseApplication;
 import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.experiments.Experiments;
 import com.android.tv.common.feature.CommonFeatures;
 import com.android.tv.common.ui.setup.SetupActivity;
 import com.android.tv.common.ui.setup.SetupFragment;
@@ -47,12 +45,14 @@
 import com.android.tv.common.util.AutoCloseableUtils;
 import com.android.tv.common.util.PostalCodeUtils;
 import com.android.tv.tuner.R;
-import com.android.tv.tuner.TunerHal;
-import com.android.tv.tuner.TunerPreferences;
+import com.android.tv.tuner.api.Tuner;
+import com.android.tv.tuner.api.TunerFactory;
+import com.android.tv.tuner.prefs.TunerPreferences;
 import java.util.concurrent.Executor;
+import javax.inject.Inject;
 
 /** The base setup activity class for tuner. */
-public class BaseTunerSetupActivity extends SetupActivity {
+public abstract class BaseTunerSetupActivity extends SetupActivity {
     private static final String TAG = "BaseTunerSetupActivity";
     private static final boolean DEBUG = false;
 
@@ -86,27 +86,21 @@
     protected String mPreviousPostalCode;
     protected boolean mActivityStopped;
     protected boolean mPendingShowInitialFragment;
+    @Inject protected TunerFactory mTunerFactory;
 
-    private TunerHalFactory mTunerHalFactory;
+    private TunerHalCreator mTunerHalCreator;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         if (DEBUG) {
             Log.d(TAG, "onCreate");
         }
+        super.onCreate(savedInstanceState);
         mActivityStopped = false;
         executeGetTunerTypeAndCountAsyncTask();
-        mTunerHalFactory =
-                new TunerHalFactory(getApplicationContext(), AsyncTask.THREAD_POOL_EXECUTOR);
-        super.onCreate(savedInstanceState);
-        // TODO: check {@link shouldShowRequestPermissionRationale}.
-        if (checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION)
-                != PackageManager.PERMISSION_GRANTED) {
-            // No need to check the request result.
-            requestPermissions(
-                    new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION},
-                    PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION);
-        }
+        mTunerHalCreator =
+                new TunerHalCreator(
+                        getApplicationContext(), AsyncTask.THREAD_POOL_EXECUTOR, mTunerFactory);
         try {
             // Updating postal code takes time, therefore we called it here for "warm-up".
             mPreviousPostalCode = PostalCodeUtils.getLastPostalCode(this);
@@ -138,25 +132,6 @@
     }
 
     @Override
-    public void onRequestPermissionsResult(
-            int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
-        if (requestCode == PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION) {
-            if (grantResults.length > 0
-                    && grantResults[0] == PackageManager.PERMISSION_GRANTED
-                    && Experiments.CLOUD_EPG.get()) {
-                try {
-                    // Updating postal code takes time, therefore we should update postal code
-                    // right after the permission is granted, so that the subsequent operations,
-                    // especially EPG fetcher, could get the newly updated postal code.
-                    PostalCodeUtils.updatePostalCode(this);
-                } catch (Exception e) {
-                    // Do nothing
-                }
-            }
-        }
-    }
-
-    @Override
     protected Fragment onCreateInitialFragment() {
         if (mTunerType != null) {
             SetupFragment fragment = new WelcomeFragment();
@@ -184,10 +159,16 @@
                         break;
                     default:
                         String postalCode = PostalCodeUtils.getLastPostalCode(this);
-                        if (mNeedToShowPostalCodeFragment
-                                || (CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(
+                        boolean needLocation =
+                                CommonFeatures.ENABLE_CLOUD_EPG_REGION.isEnabled(
                                                 getApplicationContext())
-                                        && TextUtils.isEmpty(postalCode))) {
+                                        && TextUtils.isEmpty(postalCode);
+                        if (needLocation
+                                && checkSelfPermission(
+                                                android.Manifest.permission.ACCESS_COARSE_LOCATION)
+                                        != PackageManager.PERMISSION_GRANTED) {
+                            showLocationFragment();
+                        } else if (mNeedToShowPostalCodeFragment || needLocation) {
                             // We cannot get postal code automatically. Postal code input fragment
                             // should always be shown even if users have input some valid postal
                             // code in this activity before.
@@ -199,6 +180,23 @@
                         break;
                 }
                 return true;
+            case LocationFragment.ACTION_CATEGORY:
+                switch (actionId) {
+                    case LocationFragment.ACTION_ALLOW_PERMISSION:
+                        String postalCode =
+                                params == null
+                                        ? null
+                                        : params.getString(LocationFragment.KEY_POSTAL_CODE);
+                        if (postalCode == null) {
+                            showPostalCodeFragment();
+                        } else {
+                            showConnectionTypeFragment();
+                        }
+                        break;
+                    default:
+                        showConnectionTypeFragment();
+                }
+                return true;
             case PostalCodeFragment.ACTION_CATEGORY:
                 switch (actionId) {
                     case SetupMultiPaneFragment.ACTION_DONE:
@@ -210,7 +208,7 @@
                 }
                 return true;
             case ConnectionTypeFragment.ACTION_CATEGORY:
-                if (mTunerHalFactory.getOrCreate() == null) {
+                if (mTunerHalCreator.getOrCreate() == null) {
                     finish();
                     Toast.makeText(
                                     getApplicationContext(),
@@ -233,7 +231,7 @@
                         getFragmentManager().popBackStack();
                         return true;
                     case ScanFragment.ACTION_FINISH:
-                        mTunerHalFactory.clear();
+                        mTunerHalCreator.clear();
                         showScanResultFragment();
                         return true;
                     default: // fall out
@@ -269,22 +267,36 @@
     }
 
     /** Gets the currently used tuner HAL. */
-    TunerHal getTunerHal() {
-        return mTunerHalFactory.getOrCreate();
+    Tuner getTunerHal() {
+        return mTunerHalCreator.getOrCreate();
     }
 
     /** Generates tuner HAL. */
     void generateTunerHal() {
-        mTunerHalFactory.generate();
+        mTunerHalCreator.generate();
     }
 
     /** Clears the currently used tuner HAL. */
     protected void clearTunerHal() {
-        mTunerHalFactory.clear();
+        mTunerHalCreator.clear();
+    }
+
+    protected void showLocationFragment() {
+        SetupFragment fragment = new LocationFragment();
+        fragment.setShortDistance(
+                SetupFragment.FRAGMENT_ENTER_TRANSITION | SetupFragment.FRAGMENT_RETURN_TRANSITION);
+        showFragment(fragment, true);
     }
 
     protected void showPostalCodeFragment() {
+        showPostalCodeFragment(null);
+    }
+
+    protected void showPostalCodeFragment(Bundle args) {
         SetupFragment fragment = new PostalCodeFragment();
+        if (args != null) {
+            fragment.setArguments(args);
+        }
         fragment.setShortDistance(
                 SetupFragment.FRAGMENT_ENTER_TRANSITION | SetupFragment.FRAGMENT_RETURN_TRANSITION);
         showFragment(fragment, true);
@@ -320,25 +332,28 @@
     /**
      * A callback to be invoked when the TvInputService is enabled or disabled.
      *
+     * @param tunerSetupIntent
      * @param context a {@link Context} instance
      * @param enabled {@code true} for the {@link TunerTvInputService} to be enabled; otherwise
      *     {@code false}
      */
-    public static void onTvInputEnabled(Context context, boolean enabled, Integer tunerType) {
+    public static void onTvInputEnabled(
+            Context context, boolean enabled, Integer tunerType, Intent tunerSetupIntent) {
         // Send a notification for tuner setup if there's no channels and the tuner TV input
         // setup has been not done.
         boolean channelScanDoneOnPreference = TunerPreferences.isScanDone(context);
         int channelCountOnPreference = TunerPreferences.getScannedChannelCount(context);
         if (enabled && !channelScanDoneOnPreference && channelCountOnPreference == 0) {
             TunerPreferences.setShouldShowSetupActivity(context, true);
-            sendNotification(context, tunerType);
+            sendNotification(context, tunerType, tunerSetupIntent);
         } else {
             TunerPreferences.setShouldShowSetupActivity(context, false);
             cancelNotification(context);
         }
     }
 
-    private static void sendNotification(Context context, Integer tunerType) {
+    private static void sendNotification(
+            Context context, Integer tunerType, Intent tunerSetupIntent) {
         SoftPreconditions.checkState(
                 tunerType != null, TAG, "tunerType is null when send notification");
         if (tunerType == null) {
@@ -348,29 +363,29 @@
         String contentTitle = resources.getString(R.string.ut_setup_notification_content_title);
         int contentTextId = 0;
         switch (tunerType) {
-            case TunerHal.TUNER_TYPE_BUILT_IN:
+            case Tuner.TUNER_TYPE_BUILT_IN:
                 contentTextId = R.string.bt_setup_notification_content_text;
                 break;
-            case TunerHal.TUNER_TYPE_USB:
+            case Tuner.TUNER_TYPE_USB:
                 contentTextId = R.string.ut_setup_notification_content_text;
                 break;
-            case TunerHal.TUNER_TYPE_NETWORK:
+            case Tuner.TUNER_TYPE_NETWORK:
                 contentTextId = R.string.nt_setup_notification_content_text;
                 break;
             default: // fall out
         }
         String contentText = resources.getString(contentTextId);
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-            sendNotificationInternal(context, contentTitle, contentText);
+            sendNotificationInternal(context, contentTitle, contentText, tunerSetupIntent);
         } else {
             Bitmap largeIcon =
                     BitmapFactory.decodeResource(resources, R.drawable.recommendation_antenna);
-            sendRecommendationCard(context, contentTitle, contentText, largeIcon);
+            sendRecommendationCard(context, contentTitle, contentText, largeIcon, tunerSetupIntent);
         }
     }
 
     private static void sendNotificationInternal(
-            Context context, String contentTitle, String contentText) {
+            Context context, String contentTitle, String contentText, Intent tunerSetupIntent) {
         NotificationManager notificationManager =
                 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
         notificationManager.createNotificationChannel(
@@ -387,7 +402,8 @@
                                 context.getResources()
                                         .getIdentifier(
                                                 TAG_ICON, TAG_DRAWABLE, context.getPackageName()))
-                        .setContentIntent(createPendingIntentForSetupActivity(context))
+                        .setContentIntent(
+                                createPendingIntentForSetupActivity(context, tunerSetupIntent))
                         .setVisibility(Notification.VISIBILITY_PUBLIC)
                         .extend(new Notification.TvExtender())
                         .build();
@@ -397,10 +413,15 @@
     /**
      * Sends the recommendation card to start the tuner TV input setup activity.
      *
+     * @param tunerSetupIntent
      * @param context a {@link Context} instance
      */
     private static void sendRecommendationCard(
-            Context context, String contentTitle, String contentText, Bitmap largeIcon) {
+            Context context,
+            String contentTitle,
+            String contentText,
+            Bitmap largeIcon,
+            Intent tunerSetupIntent) {
         // Build and send the notification.
         Notification notification =
                 new NotificationCompat.BigPictureStyle(
@@ -418,7 +439,8 @@
                                                                 TAG_DRAWABLE,
                                                                 context.getPackageName()))
                                         .setContentIntent(
-                                                createPendingIntentForSetupActivity(context)))
+                                                createPendingIntentForSetupActivity(
+                                                        context, tunerSetupIntent)))
                         .build();
         NotificationManager notificationManager =
                 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
@@ -429,30 +451,27 @@
      * Returns a {@link PendingIntent} to launch the tuner TV input service.
      *
      * @param context a {@link Context} instance
+     * @param tunerSetupIntent
      */
-    private static PendingIntent createPendingIntentForSetupActivity(Context context) {
+    private static PendingIntent createPendingIntentForSetupActivity(
+            Context context, Intent tunerSetupIntent) {
         return PendingIntent.getActivity(
-                context,
-                0,
-                BaseApplication.getSingletons(context).getTunerSetupIntent(context),
-                PendingIntent.FLAG_UPDATE_CURRENT);
+                context, 0, tunerSetupIntent, PendingIntent.FLAG_UPDATE_CURRENT);
     }
 
-    /** A static factory for {@link TunerHal} instances * */
+    /** Creates {@link Tuner} instances in a worker thread * */
     @VisibleForTesting
-    protected static class TunerHalFactory {
+    protected static class TunerHalCreator {
         private Context mContext;
-        @VisibleForTesting TunerHal mTunerHal;
-        private TunerHalFactory.GenerateTunerHalTask mGenerateTunerHalTask;
+        @VisibleForTesting Tuner mTunerHal;
+        private TunerHalCreator.GenerateTunerHalTask mGenerateTunerHalTask;
         private final Executor mExecutor;
+        private final TunerFactory mTunerFactory;
 
-        TunerHalFactory(Context context) {
-            this(context, AsyncTask.SERIAL_EXECUTOR);
-        }
-
-        TunerHalFactory(Context context, Executor executor) {
+        TunerHalCreator(Context context, Executor executor, TunerFactory tunerFactory) {
             mContext = context;
             mExecutor = executor;
+            mTunerFactory = tunerFactory;
         }
 
         /**
@@ -460,7 +479,7 @@
          * before, tries to generate it synchronously.
          */
         @WorkerThread
-        TunerHal getOrCreate() {
+        Tuner getOrCreate() {
             if (mGenerateTunerHalTask != null
                     && mGenerateTunerHalTask.getStatus() != AsyncTask.Status.FINISHED) {
                 try {
@@ -478,7 +497,7 @@
         @MainThread
         void generate() {
             if (mGenerateTunerHalTask == null && mTunerHal == null) {
-                mGenerateTunerHalTask = new TunerHalFactory.GenerateTunerHalTask();
+                mGenerateTunerHalTask = new TunerHalCreator.GenerateTunerHalTask();
                 mGenerateTunerHalTask.executeOnExecutor(mExecutor);
             }
         }
@@ -497,18 +516,18 @@
         }
 
         @WorkerThread
-        protected TunerHal createInstance() {
-            return TunerHal.createInstance(mContext);
+        protected Tuner createInstance() {
+            return mTunerFactory.createInstance(mContext);
         }
 
-        class GenerateTunerHalTask extends AsyncTask<Void, Void, TunerHal> {
+        class GenerateTunerHalTask extends AsyncTask<Void, Void, Tuner> {
             @Override
-            protected TunerHal doInBackground(Void... args) {
+            protected Tuner doInBackground(Void... args) {
                 return createInstance();
             }
 
             @Override
-            protected void onPostExecute(TunerHal tunerHal) {
+            protected void onPostExecute(Tuner tunerHal) {
                 mTunerHal = tunerHal;
             }
         }
diff --git a/tuner/src/com/android/tv/tuner/ChannelScanFileParser.java b/tuner/src/com/android/tv/tuner/setup/ChannelScanFileParser.java
similarity index 61%
rename from tuner/src/com/android/tv/tuner/ChannelScanFileParser.java
rename to tuner/src/com/android/tv/tuner/setup/ChannelScanFileParser.java
index d2ed6c3..43c584e 100644
--- a/tuner/src/com/android/tv/tuner/ChannelScanFileParser.java
+++ b/tuner/src/com/android/tv/tuner/setup/ChannelScanFileParser.java
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.tv.tuner;
+package com.android.tv.tuner.setup;
 
 import android.util.Log;
-import com.android.tv.tuner.data.nano.Channel;
+import com.android.tv.tuner.api.ScanChannel;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
@@ -26,49 +26,9 @@
 import java.util.List;
 
 /** Parses plain text formatted scan files, which contain the list of channels. */
-public class ChannelScanFileParser {
+public final class ChannelScanFileParser {
     private static final String TAG = "ChannelScanFileParser";
 
-    public static final class ScanChannel {
-        public final int type;
-        public final int frequency;
-        public final String modulation;
-        public final String filename;
-        /**
-         * Radio frequency (channel) number specified at
-         * https://en.wikipedia.org/wiki/North_American_television_frequencies This can be {@code
-         * null} for cases like cable signal.
-         */
-        public final Integer radioFrequencyNumber;
-
-        public static ScanChannel forTuner(
-                int frequency, String modulation, Integer radioFrequencyNumber) {
-            return new ScanChannel(
-                    Channel.TunerType.TYPE_TUNER,
-                    frequency,
-                    modulation,
-                    null,
-                    radioFrequencyNumber);
-        }
-
-        public static ScanChannel forFile(int frequency, String filename) {
-            return new ScanChannel(Channel.TunerType.TYPE_FILE, frequency, "file:", filename, null);
-        }
-
-        private ScanChannel(
-                int type,
-                int frequency,
-                String modulation,
-                String filename,
-                Integer radioFrequencyNumber) {
-            this.type = type;
-            this.frequency = frequency;
-            this.modulation = modulation;
-            this.filename = filename;
-            this.radioFrequencyNumber = radioFrequencyNumber;
-        }
-    }
-
     /**
      * Parses a given scan file and returns the list of {@link ScanChannel} objects.
      *
@@ -105,4 +65,6 @@
         }
         return scanChannelList;
     }
+
+   private ChannelScanFileParser(){}
 }
diff --git a/tuner/src/com/android/tv/tuner/setup/LiveTvTunerSetupActivity.java b/tuner/src/com/android/tv/tuner/setup/LiveTvTunerSetupActivity.java
index 722de7c..741edc7 100644
--- a/tuner/src/com/android/tv/tuner/setup/LiveTvTunerSetupActivity.java
+++ b/tuner/src/com/android/tv/tuner/setup/LiveTvTunerSetupActivity.java
@@ -17,20 +17,37 @@
 package com.android.tv.tuner.setup;
 
 import android.app.FragmentManager;
+import android.content.pm.PackageManager;
 import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
 import android.view.KeyEvent;
-import com.android.tv.tuner.TunerHal;
+import com.android.tv.common.util.PostalCodeUtils;
+import dagger.android.ContributesAndroidInjector;
 
 /** An activity that serves tuner setup process. */
 public class LiveTvTunerSetupActivity extends BaseTunerSetupActivity {
     private static final String TAG = "LiveTvTunerSetupActivity";
 
     @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        // TODO(shubang): use LocationFragment
+        if (checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION)
+                != PackageManager.PERMISSION_GRANTED) {
+            // No need to check the request result.
+            requestPermissions(
+                    new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION},
+                    PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION);
+        }
+    }
+
+    @Override
     protected void executeGetTunerTypeAndCountAsyncTask() {
         new AsyncTask<Void, Void, Integer>() {
             @Override
             protected Integer doInBackground(Void... arg0) {
-                return TunerHal.getTunerTypeAndCount(LiveTvTunerSetupActivity.this).first;
+                return mTunerFactory.getTunerTypeAndCount(LiveTvTunerSetupActivity.this).first;
             }
 
             @Override
@@ -72,4 +89,31 @@
         }
         return super.onKeyUp(keyCode, event);
     }
+
+    @Override
+    public void onRequestPermissionsResult(
+            int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+        if (requestCode == PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION) {
+            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+                try {
+                    // Updating postal code takes time, therefore we should update postal code
+                    // right after the permission is granted, so that the subsequent operations,
+                    // especially EPG fetcher, could get the newly updated postal code.
+                    PostalCodeUtils.updatePostalCode(this);
+                } catch (Exception e) {
+                    // Do nothing
+                }
+            }
+        }
+    }
+
+    /**
+     * Exports {@link LiveTvTunerSetupActivity} for Dagger codegen to create the appropriate
+     * injector.
+     */
+    @dagger.Module
+    public abstract static class Module {
+        @ContributesAndroidInjector
+        abstract LiveTvTunerSetupActivity contributeLiveTvTunerSetupActivityInjector();
+    }
 }
diff --git a/tuner/src/com/android/tv/tuner/setup/LocationFragment.java b/tuner/src/com/android/tv/tuner/setup/LocationFragment.java
new file mode 100644
index 0000000..1234ae2
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/setup/LocationFragment.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.setup;
+
+import static com.android.tv.tuner.setup.BaseTunerSetupActivity.PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION;
+
+import android.content.pm.PackageManager;
+import android.location.Address;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.NonNull;
+import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
+import android.support.v17.leanback.widget.GuidedAction;
+import android.util.Log;
+
+import com.android.tv.common.ui.setup.SetupActionHelper;
+import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
+import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
+import com.android.tv.common.util.LocationUtils;
+import com.android.tv.tuner.R;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/** A fragment shows the rationale of location permission */
+public class LocationFragment extends SetupMultiPaneFragment {
+    private static final String TAG = "com.android.tv.tuner.setup.LocationFragment";
+    private static final boolean DEBUG = true;
+
+    public static final String ACTION_CATEGORY = "com.android.tv.tuner.setup.LocationFragment";
+    public static final String KEY_POSTAL_CODE = "key_postal_code";
+
+    public static final int ACTION_ALLOW_PERMISSION = 1;
+    public static final int ENTER_ZIP_CODE = 2;
+    public static final int ACTION_GETTING_LOCATION = 3;
+    public static final int GET_LOCATION_TIMEOUT_MS = 3000;
+
+    @Override
+    protected SetupGuidedStepFragment onCreateContentFragment() {
+        return new ContentFragment();
+    }
+
+    @Override
+    protected String getActionCategory() {
+        return ACTION_CATEGORY;
+    }
+
+    @Override
+    protected boolean needsDoneButton() {
+        return false;
+    }
+
+    /** The content fragment of {@link LocationFragment}. */
+    public static class ContentFragment extends SetupGuidedStepFragment
+            implements LocationUtils.OnUpdateAddressListener {
+        private final List<GuidedAction> mGettingLocationAction = new ArrayList<>();
+        private final Handler mHandler = new Handler();
+        private final Object mPostalCodeLock = new Object();
+
+        private String mPostalCode;
+        private boolean mPermissionGranted;
+
+        private final Runnable mTimeoutRunnable =
+                () -> {
+                    synchronized (mPostalCodeLock) {
+                        if (DEBUG) {
+                            Log.d(TAG,
+                                    "get location timeout. mPostalCode=" + mPostalCode);
+                        }
+                        if (mPostalCode == null) {
+                            // timeout. setup activity will get null postal code
+                            LocationUtils.removeOnUpdateAddressListener(this);
+                            passPostalCode();
+                        }
+                    }
+                };
+
+        @NonNull
+        @Override
+        public Guidance onCreateGuidance(Bundle savedInstanceState) {
+            String title = getString(R.string.location_guidance_title);
+            String description = getString(R.string.location_guidance_description);
+            return new Guidance(title, description, getString(R.string.ut_setup_breadcrumb), null);
+        }
+
+        @Override
+        public void onCreateActions(
+                @NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
+            actions.add(
+                    new GuidedAction.Builder(getActivity())
+                            .id(ACTION_ALLOW_PERMISSION)
+                            .title(getString(R.string.location_choices_allow_permission))
+                            .build());
+            actions.add(
+                    new GuidedAction.Builder(getActivity())
+                            .id(ENTER_ZIP_CODE)
+                            .title(getString(R.string.location_choices_enter_zip_code))
+                            .build());
+            actions.add(
+                    new GuidedAction.Builder(getActivity())
+                            .id(ACTION_SKIP)
+                            .title(getString(com.android.tv.common.R.string.action_text_skip))
+                            .build());
+            mGettingLocationAction.add(
+                    new GuidedAction.Builder(getActivity())
+                            .id(ACTION_GETTING_LOCATION)
+                            .title(getString(R.string.location_choices_getting_location))
+                            .focusable(false)
+                            .build()
+            );
+        }
+
+        @Override
+        public void onGuidedActionClicked(GuidedAction action) {
+            if (DEBUG) {
+                Log.d(TAG, "onGuidedActionClicked. Action ID = " + action.getId());
+            }
+            if (action.getId() == ACTION_ALLOW_PERMISSION) {
+                // request permission when users click this action
+                mPermissionGranted = false;
+                requestPermissions(
+                        new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION},
+                        PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION);
+            } else {
+                super.onGuidedActionClicked(action);
+            }
+        }
+
+        @Override
+        protected String getActionCategory() {
+            return ACTION_CATEGORY;
+        }
+
+        @Override
+        public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
+                @NonNull int[] grantResults) {
+            if (requestCode == PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION) {
+                if (grantResults.length > 0
+                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+                    synchronized (mPostalCodeLock) {
+                        mPermissionGranted = true;
+                        if (mPostalCode == null) {
+                            // get postal code immediately if available
+                            try {
+                                Address address = LocationUtils.getCurrentAddress(getActivity());
+                                if (address != null) {
+                                    mPostalCode = address.getPostalCode();
+                                }
+                            } catch (IOException e) {
+                                // do nothing
+                            }
+                        }
+                        if (DEBUG) {
+                            Log.d(TAG, "permission granted. mPostalCode=" + mPostalCode);
+                        }
+                        if (mPostalCode != null) {
+                            // if postal code is known, pass it the setup activity
+                            LocationUtils.removeOnUpdateAddressListener(this);
+                            passPostalCode();
+                        } else {
+                            // show "getting location" message
+                            setActions(mGettingLocationAction);
+                            // post timeout runnable
+                            mHandler.postDelayed(mTimeoutRunnable, GET_LOCATION_TIMEOUT_MS);
+                        }
+                    }
+                }
+            }
+        }
+
+        @Override
+        public boolean onUpdateAddress(Address address) {
+            synchronized (mPostalCodeLock) {
+                // it takes time to get location after the permission is granted,
+                // so this listener is needed
+                mPostalCode = address.getPostalCode();
+                if (DEBUG) {
+                    Log.d(TAG, "onUpdateAddress. mPostalCode=" + mPostalCode);
+                }
+                if (mPermissionGranted && mPostalCode != null) {
+                    // pass the postal code only if permission is granted
+                    passPostalCode();
+                    return true;
+                }
+                return false;
+            }
+        }
+
+        @Override
+        public void onResume() {
+            if (DEBUG) {
+                Log.d(TAG, "onResume");
+            }
+            super.onResume();
+            LocationUtils.addOnUpdateAddressListener(this);
+        }
+
+        @Override
+        public void onPause() {
+            if (DEBUG) {
+                Log.d(TAG, "onPause");
+            }
+            LocationUtils.removeOnUpdateAddressListener(this);
+            mHandler.removeCallbacks(mTimeoutRunnable);
+            super.onPause();
+        }
+
+        private void passPostalCode() {
+            synchronized (mPostalCodeLock) {
+                mHandler.removeCallbacks(mTimeoutRunnable);
+                Bundle params = new Bundle();
+                if (mPostalCode != null) {
+                    params.putString(KEY_POSTAL_CODE, mPostalCode);
+                }
+                SetupActionHelper.onActionClick(
+                        this, ACTION_CATEGORY, ACTION_ALLOW_PERMISSION, params);
+            }
+        }
+    }
+}
diff --git a/tuner/src/com/android/tv/tuner/setup/PostalCodeFragment.java b/tuner/src/com/android/tv/tuner/setup/PostalCodeFragment.java
index f4b9f65..5224797 100644
--- a/tuner/src/com/android/tv/tuner/setup/PostalCodeFragment.java
+++ b/tuner/src/com/android/tv/tuner/setup/PostalCodeFragment.java
@@ -32,10 +32,11 @@
 import com.android.tv.tuner.R;
 import java.util.List;
 
-/** A fragment for initial screen. */
+/** A fragment for users to enter postal code. */
 public class PostalCodeFragment extends SetupMultiPaneFragment {
     public static final String ACTION_CATEGORY = "com.android.tv.tuner.setup.PostalCodeFragment";
     public static final String KEY_POSTAL_CODE = "postal_code";
+    public static final String KEY_GET_LOCATION_FAILED = "get_location_failed";
     private static final int VIEW_TYPE_EDITABLE = 1;
 
     @Override
@@ -43,6 +44,11 @@
         ContentFragment fragment = new ContentFragment();
         Bundle arguments = new Bundle();
         arguments.putBoolean(SetupGuidedStepFragment.KEY_THREE_PANE, true);
+        if (getArguments() != null) {
+            arguments.putBoolean(
+                    KEY_GET_LOCATION_FAILED,
+                    getArguments().getBoolean(KEY_GET_LOCATION_FAILED, false));
+        }
         fragment.setArguments(arguments);
         return fragment;
     }
@@ -139,9 +145,16 @@
         @Override
         public Guidance onCreateGuidance(Bundle savedInstanceState) {
             String title = getString(R.string.postal_code_guidance_title);
-            String description = getString(R.string.postal_code_guidance_description);
+            StringBuilder description = new StringBuilder();
+            if (getArguments().getBoolean(KEY_GET_LOCATION_FAILED, false)) {
+                description
+                        .append(getString(R.string
+                                .postal_code_guidance_description_get_location_failed))
+                        .append(" ");
+            }
+            description.append(getString(R.string.postal_code_guidance_description));
             String breadcrumb = getString(R.string.ut_setup_breadcrumb);
-            return new Guidance(title, description, breadcrumb, null);
+            return new Guidance(title, description.toString(), breadcrumb, null);
         }
 
         @Override
diff --git a/tuner/src/com/android/tv/tuner/setup/ScanFragment.java b/tuner/src/com/android/tv/tuner/setup/ScanFragment.java
index 3ac86e1..7d59284 100644
--- a/tuner/src/com/android/tv/tuner/setup/ScanFragment.java
+++ b/tuner/src/com/android/tv/tuner/setup/ScanFragment.java
@@ -37,21 +37,21 @@
 import android.widget.TextView;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.ui.setup.SetupFragment;
-import com.android.tv.tuner.ChannelScanFileParser;
 import com.android.tv.tuner.R;
-import com.android.tv.tuner.TunerHal;
-import com.android.tv.tuner.TunerPreferences;
+import com.android.tv.tuner.api.ScanChannel;
+import com.android.tv.tuner.api.Tuner;
 import com.android.tv.tuner.data.PsipData;
 import com.android.tv.tuner.data.TunerChannel;
 import com.android.tv.tuner.data.nano.Channel;
 
 
+import com.android.tv.tuner.prefs.TunerPreferences;
 import com.android.tv.tuner.source.FileTsStreamer;
 import com.android.tv.tuner.source.TsDataSource;
 import com.android.tv.tuner.source.TsStreamer;
 import com.android.tv.tuner.source.TunerTsStreamer;
-import com.android.tv.tuner.tvinput.ChannelDataManager;
-import com.android.tv.tuner.tvinput.EventDetector;
+import com.android.tv.tuner.ts.EventDetector;
+import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
@@ -99,7 +99,7 @@
         if (DEBUG) Log.d(TAG, "onCreateView");
         View view = super.onCreateView(inflater, container, savedInstanceState);
         mChannelNumbers = new ArrayList<>();
-        mChannelDataManager = new ChannelDataManager(getActivity());
+        mChannelDataManager = new ChannelDataManager(getActivity().getApplicationContext());
         mChannelDataManager.checkDataVersion(getActivity());
         mAdapter = new ChannelAdapter();
         mProgressBar = (ProgressBar) view.findViewById(R.id.tune_progress);
@@ -126,10 +126,10 @@
         startScan(args == null ? 0 : args.getInt(EXTRA_FOR_CHANNEL_SCAN_FILE, 0));
         TextView scanTitleView = (TextView) view.findViewById(R.id.tune_title);
         switch (tunerType) {
-            case TunerHal.TUNER_TYPE_USB:
+            case Tuner.TUNER_TYPE_USB:
                 scanTitleView.setText(R.string.ut_channel_scan);
                 break;
-            case TunerHal.TUNER_TYPE_NETWORK:
+            case Tuner.TUNER_TYPE_NETWORK:
                 scanTitleView.setText(R.string.nt_channel_scan);
                 break;
             default:
@@ -176,12 +176,9 @@
             // Notifies a user of waiting to finish the scanning process.
             new Handler()
                     .postDelayed(
-                            new Runnable() {
-                                @Override
-                                public void run() {
-                                    if (mChannelScanTask != null) {
-                                        mChannelScanTask.showFinishingProgressDialog();
-                                    }
+                            () -> {
+                                if (mChannelScanTask != null) {
+                                    mChannelScanTask.showFinishingProgressDialog();
                                 }
                             },
                             SHOW_PROGRESS_DIALOG_DELAY_MS);
@@ -248,7 +245,7 @@
     }
 
     private class ChannelScanTask extends AsyncTask<Void, Integer, Void>
-            implements EventDetector.EventListener, ChannelDataManager.ChannelScanListener {
+            implements EventDetector.EventListener, ChannelDataManager.ChannelHandlingDoneListener {
         private static final int MAX_PROGRESS = 100;
 
         private final Activity mActivity;
@@ -257,7 +254,7 @@
         private final TsStreamer mFileTsStreamer;
         private final ConditionVariable mConditionStopped;
 
-        private final List<ChannelScanFileParser.ScanChannel> mScanChannelList = new ArrayList<>();
+        private final List<ScanChannel> mScanChannelList = new ArrayList<>();
         private boolean mIsCanceled;
         private boolean mIsFinished;
         private ProgressDialog mFinishingProgressDialog;
@@ -269,7 +266,7 @@
             if (FAKE_MODE) {
                 mScanTsStreamer = new FakeTsStreamer(this);
             } else {
-                TunerHal hal = ((BaseTunerSetupActivity) mActivity).getTunerHal();
+                Tuner hal = ((BaseTunerSetupActivity) mActivity).getTunerHal();
                 if (hal == null) {
                     throw new RuntimeException("Failed to open a DVB device");
                 }
@@ -282,41 +279,35 @@
 
         private void maybeSetChannelListVisible() {
             mActivity.runOnUiThread(
-                    new Runnable() {
-                        @Override
-                        public void run() {
-                            int channelsFound = mAdapter.getCount();
-                            if (!mChannelListVisible && channelsFound > 0) {
-                                String format =
-                                        getResources()
-                                                .getQuantityString(
-                                                        R.plurals.ut_channel_scan_message,
-                                                        channelsFound,
-                                                        channelsFound);
-                                mScanningMessage.setText(String.format(format, channelsFound));
-                                mChannelHolder.setVisibility(View.VISIBLE);
-                                mChannelListVisible = true;
-                            }
+                    () -> {
+                        int channelsFound = mAdapter.getCount();
+                        if (!mChannelListVisible && channelsFound > 0) {
+                            String format =
+                                    getResources()
+                                            .getQuantityString(
+                                                    R.plurals.ut_channel_scan_message,
+                                                    channelsFound,
+                                                    channelsFound);
+                            mScanningMessage.setText(String.format(format, channelsFound));
+                            mChannelHolder.setVisibility(View.VISIBLE);
+                            mChannelListVisible = true;
                         }
                     });
         }
 
         private void addChannel(final TunerChannel channel) {
             mActivity.runOnUiThread(
-                    new Runnable() {
-                        @Override
-                        public void run() {
-                            mAdapter.add(channel);
-                            if (mChannelListVisible) {
-                                int channelsFound = mAdapter.getCount();
-                                String format =
-                                        getResources()
-                                                .getQuantityString(
-                                                        R.plurals.ut_channel_scan_message,
-                                                        channelsFound,
-                                                        channelsFound);
-                                mScanningMessage.setText(String.format(format, channelsFound));
-                            }
+                    () -> {
+                        mAdapter.add(channel);
+                        if (mChannelListVisible) {
+                            int channelsFound = mAdapter.getCount();
+                            String format =
+                                    getResources()
+                                            .getQuantityString(
+                                                    R.plurals.ut_channel_scan_message,
+                                                    channelsFound,
+                                                    channelsFound);
+                            mScanningMessage.setText(String.format(format, channelsFound));
                         }
                     });
         }
@@ -366,7 +357,7 @@
 
             long startMs = System.currentTimeMillis();
             int i = 1;
-            for (ChannelScanFileParser.ScanChannel scanChannel : mScanChannelList) {
+            for (ScanChannel scanChannel : mScanChannelList) {
                 int frequency = scanChannel.frequency;
                 String modulation = scanChannel.modulation;
                 Log.i(TAG, "Tuning to " + frequency + " " + modulation);
@@ -403,7 +394,7 @@
             if (DEBUG) Log.i(TAG, "Channel scan ended");
         }
 
-        private void addChannelsWithoutVct(ChannelScanFileParser.ScanChannel scanChannel) {
+        private void addChannelsWithoutVct(ScanChannel scanChannel) {
             if (scanChannel.radioFrequencyNumber == null
                     || !(mScanTsStreamer instanceof TunerTsStreamer)) {
                 return;
@@ -515,7 +506,7 @@
         }
 
         @Override
-        public boolean startStream(ChannelScanFileParser.ScanChannel channel) {
+        public boolean startStream(ScanChannel channel) {
             if (++mProgramNumber % 2 == 1) {
                 return true;
             }
diff --git a/tuner/src/com/android/tv/tuner/setup/ScanResultFragment.java b/tuner/src/com/android/tv/tuner/setup/ScanResultFragment.java
index 480bf08..bd3f9ad 100644
--- a/tuner/src/com/android/tv/tuner/setup/ScanResultFragment.java
+++ b/tuner/src/com/android/tv/tuner/setup/ScanResultFragment.java
@@ -25,11 +25,11 @@
 import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
 import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
 import com.android.tv.tuner.R;
-import com.android.tv.tuner.TunerHal;
-import com.android.tv.tuner.TunerPreferences;
+import com.android.tv.tuner.api.Tuner;
+import com.android.tv.tuner.prefs.TunerPreferences;
 import java.util.List;
 
-/** A fragment for initial screen. */
+/** A fragment to show found channels. */
 public class ScanResultFragment extends SetupMultiPaneFragment {
     public static final String ACTION_CATEGORY = "com.android.tv.tuner.setup.ScanResultFragment";
 
@@ -83,10 +83,10 @@
                         (args == null ? 0 : args.getInt(BaseTunerSetupActivity.KEY_TUNER_TYPE, 0));
                 title = getString(R.string.ut_result_not_found_title);
                 switch (tunerType) {
-                    case TunerHal.TUNER_TYPE_USB:
+                    case Tuner.TUNER_TYPE_USB:
                         description = getString(R.string.ut_result_not_found_description);
                         break;
-                    case TunerHal.TUNER_TYPE_NETWORK:
+                    case Tuner.TUNER_TYPE_NETWORK:
                         description = getString(R.string.nt_result_not_found_description);
                         break;
                     default:
diff --git a/tuner/src/com/android/tv/tuner/setup/WelcomeFragment.java b/tuner/src/com/android/tv/tuner/setup/WelcomeFragment.java
index 788ba91..2a414df 100644
--- a/tuner/src/com/android/tv/tuner/setup/WelcomeFragment.java
+++ b/tuner/src/com/android/tv/tuner/setup/WelcomeFragment.java
@@ -24,8 +24,8 @@
 import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
 import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
 import com.android.tv.tuner.R;
-import com.android.tv.tuner.TunerHal;
-import com.android.tv.tuner.TunerPreferences;
+import com.android.tv.tuner.api.Tuner;
+import com.android.tv.tuner.prefs.TunerPreferences;
 import java.util.List;
 
 /** A fragment for initial screen. */
@@ -69,14 +69,14 @@
                     getArguments()
                             .getInt(
                                     BaseTunerSetupActivity.KEY_TUNER_TYPE,
-                                    TunerHal.TUNER_TYPE_BUILT_IN);
+                                    Tuner.TUNER_TYPE_BUILT_IN);
             if (mChannelCountOnPreference == 0) {
                 switch (tunerType) {
-                    case TunerHal.TUNER_TYPE_USB:
+                    case Tuner.TUNER_TYPE_USB:
                         title = getString(R.string.ut_setup_new_title);
                         description = getString(R.string.ut_setup_new_description);
                         break;
-                    case TunerHal.TUNER_TYPE_NETWORK:
+                    case Tuner.TUNER_TYPE_NETWORK:
                         title = getString(R.string.nt_setup_new_title);
                         description = getString(R.string.nt_setup_new_description);
                         break;
@@ -87,10 +87,10 @@
             } else {
                 title = getString(R.string.bt_setup_again_title);
                 switch (tunerType) {
-                    case TunerHal.TUNER_TYPE_USB:
+                    case Tuner.TUNER_TYPE_USB:
                         description = getString(R.string.ut_setup_again_description);
                         break;
-                    case TunerHal.TUNER_TYPE_NETWORK:
+                    case Tuner.TUNER_TYPE_NETWORK:
                         description = getString(R.string.nt_setup_again_description);
                         break;
                     default:
diff --git a/src/com/android/tv/util/Filter.java b/tuner/src/com/android/tv/tuner/singletons/TunerSingletons.java
similarity index 67%
copy from src/com/android/tv/util/Filter.java
copy to tuner/src/com/android/tv/tuner/singletons/TunerSingletons.java
index 3e24a49..48b17dc 100644
--- a/src/com/android/tv/util/Filter.java
+++ b/tuner/src/com/android/tv/tuner/singletons/TunerSingletons.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,11 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package com.android.tv.tuner.singletons;
 
-package com.android.tv.util;
+import com.android.tv.common.singletons.HasTvInputId;
 
-/** Interface to decide whether an input is filtered out or not. */
-public interface Filter<T> {
-    /** Returns true, if {@code input} is acceptable. */
-    boolean filter(T input);
-}
+/** Singletons used in tuner applications */
+public interface TunerSingletons extends HasTvInputId {}
diff --git a/tuner/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java b/tuner/src/com/android/tv/tuner/source/FileSourceEventDetector.java
similarity index 97%
rename from tuner/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java
rename to tuner/src/com/android/tv/tuner/source/FileSourceEventDetector.java
index ab05aa0..85932c8 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java
+++ b/tuner/src/com/android/tv/tuner/source/FileSourceEventDetector.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.tv.tuner.tvinput;
+package com.android.tv.tuner.source;
 
 import android.util.Log;
 import android.util.SparseArray;
@@ -27,9 +27,8 @@
 import com.android.tv.tuner.data.TunerChannel;
 import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
 import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
-import com.android.tv.tuner.source.FileTsStreamer;
+import com.android.tv.tuner.ts.EventDetector.EventListener;
 import com.android.tv.tuner.ts.TsParser;
-import com.android.tv.tuner.tvinput.EventDetector.EventListener;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -57,7 +56,7 @@
     private FileTsStreamer.StreamProvider mStreamProvider;
     private int mProgramNumber = ALL_PROGRAM_NUMBERS;
 
-    public FileSourceEventDetector(EventDetector.EventListener listener, boolean enableDvbSignal) {
+    public FileSourceEventDetector(EventListener listener, boolean enableDvbSignal) {
         mEventListener = listener;
         mEnableDvbSignal = enableDvbSignal;
     }
diff --git a/tuner/src/com/android/tv/tuner/source/FileTsStreamer.java b/tuner/src/com/android/tv/tuner/source/FileTsStreamer.java
index 38a59b3..99d37e3 100644
--- a/tuner/src/com/android/tv/tuner/source/FileTsStreamer.java
+++ b/tuner/src/com/android/tv/tuner/source/FileTsStreamer.java
@@ -21,12 +21,11 @@
 import android.util.Log;
 import android.util.SparseBooleanArray;
 import com.android.tv.common.SoftPreconditions;
-import com.android.tv.tuner.ChannelScanFileParser.ScanChannel;
-import com.android.tv.tuner.TunerFeatures;
+import com.android.tv.tuner.api.ScanChannel;
 import com.android.tv.tuner.data.TunerChannel;
+import com.android.tv.tuner.features.TunerFeatures;
+import com.android.tv.tuner.ts.EventDetector.EventListener;
 import com.android.tv.tuner.ts.TsParser;
-import com.android.tv.tuner.tvinput.EventDetector;
-import com.android.tv.tuner.tvinput.FileSourceEventDetector;
 import com.google.android.exoplayer.C;
 import com.google.android.exoplayer.upstream.DataSpec;
 import java.io.BufferedInputStream;
@@ -125,7 +124,7 @@
      *
      * @param eventListener the listener for channel & program information
      */
-    public FileTsStreamer(EventDetector.EventListener eventListener, Context context) {
+    public FileTsStreamer(EventListener eventListener, Context context) {
         mEventDetector =
                 new FileSourceEventDetector(
                         eventListener, TunerFeatures.ENABLE_FILE_DVB.isEnabled(context));
diff --git a/tuner/src/com/android/tv/tuner/source/TsDataSource.java b/tuner/src/com/android/tv/tuner/source/TsDataSource.java
index be90294..cf3c25d 100644
--- a/tuner/src/com/android/tv/tuner/source/TsDataSource.java
+++ b/tuner/src/com/android/tv/tuner/source/TsDataSource.java
@@ -16,6 +16,7 @@
 
 package com.android.tv.tuner.source;
 
+import com.android.tv.common.compat.TvInputConstantCompat;
 import com.google.android.exoplayer.upstream.DataSource;
 
 /** {@link DataSource} for MPEG-TS stream, which will be used by {@link TsExtractor}. */
@@ -46,4 +47,8 @@
      * @param offset 0 <= offset <= buffered position
      */
     public void shiftStartPosition(long offset) {}
+
+    public int getSignalStrength() {
+        return TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED;
+    }
 }
diff --git a/tuner/src/com/android/tv/tuner/source/TsDataSourceManager.java b/tuner/src/com/android/tv/tuner/source/TsDataSourceManager.java
index 08acbc8..28756a9 100644
--- a/tuner/src/com/android/tv/tuner/source/TsDataSourceManager.java
+++ b/tuner/src/com/android/tv/tuner/source/TsDataSourceManager.java
@@ -18,49 +18,58 @@
 
 import android.content.Context;
 import android.support.annotation.VisibleForTesting;
-import com.android.tv.tuner.TunerHal;
+import com.android.tv.tuner.api.Tuner;
 import com.android.tv.tuner.data.TunerChannel;
 import com.android.tv.tuner.data.nano.Channel;
-import com.android.tv.tuner.tvinput.EventDetector;
+import com.android.tv.tuner.ts.EventDetector.EventListener;
+import com.google.auto.factory.AutoFactory;
+import com.google.auto.factory.Provided;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.inject.Inject;
+import javax.inject.Provider;
 
 /**
- * Manages {@link DataSource} for playback and recording. The class hides handling of {@link
- * TunerHal} and {@link TsStreamer} from other classes. One TsDataSourceManager should be created
- * for per session.
+ * Manages {@link TsDataSource} for playback and recording. The class hides handling of {@link
+ * Tuner} and {@link TsStreamer} from other classes. One TsDataSourceManager should be created for
+ * per session.
  */
+@AutoFactory
 public class TsDataSourceManager {
-    private static final Object sLock = new Object();
     private static final Map<TsDataSource, TsStreamer> sTsStreamers = new ConcurrentHashMap<>();
 
-    private static int sSequenceId;
+    private static final AtomicInteger sSequenceId = new AtomicInteger();
 
-    private final int mId;
+    private final int mId = sSequenceId.incrementAndGet();
     private final boolean mIsRecording;
-    private final TunerTsStreamerManager mTunerStreamerManager =
-            TunerTsStreamerManager.getInstance();
+    private final TunerTsStreamerManager mTunerStreamerManager;
 
     private boolean mKeepTuneStatus;
 
     /**
-     * Creates TsDataSourceManager to create and release {@link DataSource} which will be used for
-     * playing and recording.
+     * Factory for {@link }TsDataSourceManager}.
      *
-     * @param isRecording {@code true} when for recording, {@code false} otherwise
-     * @return {@link TsDataSourceManager}
+     * <p>This wrapper class keeps other classes from needing to reference the {@link AutoFactory}
+     * generated class.
      */
-    public static TsDataSourceManager createSourceManager(boolean isRecording) {
-        int id;
-        synchronized (sLock) {
-            id = ++sSequenceId;
+    public static final class Factory {
+        private final TsDataSourceManagerFactory mDelegate;
+
+        @Inject
+        public Factory(Provider<TunerTsStreamerManager> tunerStreamerManagerProvider) {
+            mDelegate = new TsDataSourceManagerFactory(tunerStreamerManagerProvider);
         }
-        return new TsDataSourceManager(id, isRecording);
+
+        public TsDataSourceManager create(boolean isRecording) {
+            return mDelegate.create(isRecording);
+        }
     }
 
-    private TsDataSourceManager(int id, boolean isRecording) {
-        mId = id;
+    TsDataSourceManager(
+            boolean isRecording, @Provided TunerTsStreamerManager tunerStreamerManager) {
         mIsRecording = isRecording;
+        this.mTunerStreamerManager = tunerStreamerManager;
         mKeepTuneStatus = true;
     }
 
@@ -73,7 +82,7 @@
      * @return {@link TsDataSource} which will provide the specified channel stream
      */
     public TsDataSource createDataSource(
-            Context context, TunerChannel channel, EventDetector.EventListener eventListener) {
+            Context context, TunerChannel channel, EventListener eventListener) {
         if (channel.getType() == Channel.TunerType.TYPE_FILE) {
             // MPEG2 TS captured stream file recording is not supported.
             if (mIsRecording) {
@@ -92,7 +101,7 @@
     }
 
     /**
-     * Releases the specified {@link TsDataSource} and underlying {@link TunerHal}.
+     * Releases the specified {@link TsDataSource} and underlying {@link Tuner}.
      *
      * @param source to release
      */
@@ -114,10 +123,10 @@
     }
 
     /**
-     * Indicates whether the underlying {@link TunerHal} should be kept or not when data source is
+     * Indicates whether the underlying {@link Tuner} should be kept or not when data source is
      * being released. TODO: If b/30750953 is fixed, we can remove this function.
      *
-     * @param keepTuneStatus underlying {@link TunerHal} will be reused when data source releasing.
+     * @param keepTuneStatus underlying {@link Tuner} will be reused when data source releasing.
      */
     public void setKeepTuneStatus(boolean keepTuneStatus) {
         mKeepTuneStatus = keepTuneStatus;
@@ -125,7 +134,7 @@
 
     /** Add tuner hal into TunerTsStreamerManager for test. */
     @VisibleForTesting
-    public void addTunerHalForTest(TunerHal tunerHal) {
+    public void addTunerHalForTest(Tuner tunerHal) {
         mTunerStreamerManager.addTunerHal(tunerHal, mId);
     }
 
diff --git a/tuner/src/com/android/tv/tuner/source/TsStreamer.java b/tuner/src/com/android/tv/tuner/source/TsStreamer.java
index 3dbba7e..e5658e7 100644
--- a/tuner/src/com/android/tv/tuner/source/TsStreamer.java
+++ b/tuner/src/com/android/tv/tuner/source/TsStreamer.java
@@ -16,7 +16,7 @@
 
 package com.android.tv.tuner.source;
 
-import com.android.tv.tuner.ChannelScanFileParser;
+import com.android.tv.tuner.api.ScanChannel;
 import com.android.tv.tuner.data.TunerChannel;
 
 /**
@@ -27,10 +27,10 @@
     /**
      * Starts streaming the data for channel scanning process.
      *
-     * @param channel {@link ChannelScanFileParser.ScanChannel} to be scanned
+     * @param channel {@link ScanChannel} to be scanned
      * @return {@code true} if ready to stream, otherwise {@code false}
      */
-    boolean startStream(ChannelScanFileParser.ScanChannel channel);
+    boolean startStream(ScanChannel channel);
 
     /**
      * Starts streaming the data for channel playing or recording.
diff --git a/tuner/src/com/android/tv/tuner/source/TunerSourceModule.java b/tuner/src/com/android/tv/tuner/source/TunerSourceModule.java
new file mode 100644
index 0000000..12d2de1
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/source/TunerSourceModule.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.source;
+
+import com.android.tv.tuner.api.TunerFactory;
+import dagger.Module;
+import dagger.Provides;
+import javax.inject.Singleton;
+
+/** Dagger module for TV Tuners Sources. */
+@Module()
+public class TunerSourceModule {
+    @Provides
+    @Singleton
+    TunerTsStreamerManager providesTunerTsStreamerManager(TunerFactory tunerFactory) {
+        return new TunerTsStreamerManager(tunerFactory);
+    }
+}
diff --git a/tuner/src/com/android/tv/tuner/source/TunerTsStreamer.java b/tuner/src/com/android/tv/tuner/source/TunerTsStreamer.java
index 21b7a1f..9e68c91 100644
--- a/tuner/src/com/android/tv/tuner/source/TunerTsStreamer.java
+++ b/tuner/src/com/android/tv/tuner/source/TunerTsStreamer.java
@@ -20,12 +20,12 @@
 import android.util.Log;
 import android.util.Pair;
 import com.android.tv.common.SoftPreconditions;
-import com.android.tv.tuner.ChannelScanFileParser;
-import com.android.tv.tuner.TunerHal;
-import com.android.tv.tuner.TunerPreferences;
+import com.android.tv.tuner.api.ScanChannel;
+import com.android.tv.tuner.api.Tuner;
 import com.android.tv.tuner.data.TunerChannel;
-import com.android.tv.tuner.tvinput.EventDetector;
-import com.android.tv.tuner.tvinput.EventDetector.EventListener;
+import com.android.tv.tuner.prefs.TunerPreferences;
+import com.android.tv.tuner.ts.EventDetector;
+import com.android.tv.tuner.ts.EventDetector.EventListener;
 import com.google.android.exoplayer.C;
 import com.google.android.exoplayer.upstream.DataSpec;
 import java.io.IOException;
@@ -53,7 +53,7 @@
     private final AtomicLong mLastReadPosition = new AtomicLong();
     private boolean mStreaming;
 
-    private final TunerHal mTunerHal;
+    private final Tuner mTunerHal;
     private TunerChannel mChannel;
     private Thread mStreamingThread;
     private final EventDetector mEventDetector;
@@ -121,6 +121,11 @@
             }
             return ret;
         }
+
+        @Override
+        public int getSignalStrength() {
+            return mTsStreamer.getSignalStrength();
+        }
     }
     /**
      * Creates {@link TsStreamer} for playing or recording the specified channel.
@@ -128,7 +133,7 @@
      * @param tunerHal the HAL for tuner device
      * @param eventListener the listener for channel & program information
      */
-    public TunerTsStreamer(TunerHal tunerHal, EventListener eventListener, Context context) {
+    public TunerTsStreamer(Tuner tunerHal, EventListener eventListener, Context context) {
         mTunerHal = tunerHal;
         mEventDetector = new EventDetector(mTunerHal);
         if (eventListener != null) {
@@ -140,7 +145,7 @@
                         : null;
     }
 
-    public TunerTsStreamer(TunerHal tunerHal, EventListener eventListener) {
+    public TunerTsStreamer(Tuner tunerHal, EventListener eventListener) {
         this(tunerHal, eventListener, null);
     }
 
@@ -149,20 +154,20 @@
         if (mTunerHal.tune(
                 channel.getFrequency(), channel.getModulation(), channel.getDisplayNumber(false))) {
             if (channel.hasVideo()) {
-                mTunerHal.addPidFilter(channel.getVideoPid(), TunerHal.FILTER_TYPE_VIDEO);
+                mTunerHal.addPidFilter(channel.getVideoPid(), Tuner.FILTER_TYPE_VIDEO);
             }
             boolean audioFilterSet = false;
             for (Integer audioPid : channel.getAudioPids()) {
                 if (!audioFilterSet) {
-                    mTunerHal.addPidFilter(audioPid, TunerHal.FILTER_TYPE_AUDIO);
+                    mTunerHal.addPidFilter(audioPid, Tuner.FILTER_TYPE_AUDIO);
                     audioFilterSet = true;
                 } else {
                     // FILTER_TYPE_AUDIO overrides the previous filter for audio. We use
                     // FILTER_TYPE_OTHER from the secondary one to get the all audio tracks.
-                    mTunerHal.addPidFilter(audioPid, TunerHal.FILTER_TYPE_OTHER);
+                    mTunerHal.addPidFilter(audioPid, Tuner.FILTER_TYPE_OTHER);
                 }
             }
-            mTunerHal.addPidFilter(channel.getPcrPid(), TunerHal.FILTER_TYPE_PCR);
+            mTunerHal.addPidFilter(channel.getPcrPid(), Tuner.FILTER_TYPE_PCR);
             if (mEventDetector != null) {
                 mEventDetector.startDetecting(
                         channel.getFrequency(),
@@ -193,7 +198,7 @@
     }
 
     @Override
-    public boolean startStream(ChannelScanFileParser.ScanChannel channel) {
+    public boolean startStream(ScanChannel channel) {
         if (mTunerHal.tune(channel.frequency, channel.modulation, null)) {
             mEventDetector.startDetecting(
                     channel.frequency, channel.modulation, EventDetector.ALL_PROGRAM_NUMBERS);
@@ -255,11 +260,11 @@
     }
 
     /**
-     * Returns the current {@link TunerHal} which provides MPEG-TS stream for TunerTsStreamer.
+     * Returns the current {@link Tuner} which provides MPEG-TS stream for TunerTsStreamer.
      *
-     * @return {@link TunerHal}
+     * @return {@link Tuner}
      */
-    public TunerHal getTunerHal() {
+    public Tuner getTunerHal() {
         return mTunerHal;
     }
 
@@ -303,6 +308,10 @@
         }
     }
 
+    public int getSignalStrength() {
+        return mTunerHal.getSignalStrength();
+    }
+
     private class StreamingThread extends Thread {
         @Override
         public void run() {
diff --git a/tuner/src/com/android/tv/tuner/source/TunerTsStreamerManager.java b/tuner/src/com/android/tv/tuner/source/TunerTsStreamerManager.java
index 44fb41e..076206c 100644
--- a/tuner/src/com/android/tv/tuner/source/TunerTsStreamerManager.java
+++ b/tuner/src/com/android/tv/tuner/source/TunerTsStreamerManager.java
@@ -17,52 +17,49 @@
 package com.android.tv.tuner.source;
 
 import android.content.Context;
+import android.support.annotation.VisibleForTesting;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.util.AutoCloseableUtils;
-import com.android.tv.tuner.TunerHal;
+import com.android.tv.tuner.api.Tuner;
+import com.android.tv.tuner.api.TunerFactory;
 import com.android.tv.tuner.data.TunerChannel;
-import com.android.tv.tuner.tvinput.EventDetector;
+import com.android.tv.tuner.ts.EventDetector.EventListener;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.Set;
+import javax.inject.Inject;
+import javax.inject.Singleton;
 
 /**
  * Manages {@link TunerTsStreamer} for playback and recording. The class hides handling of {@link
- * TunerHal} from other classes. This class is used by {@link TsDataSourceManager}. Don't use this
+ * Tuner} from other classes. This class is used by {@link TsDataSourceManager}. Don't use this
  * class directly.
  */
-class TunerTsStreamerManager {
+@Singleton
+@VisibleForTesting
+public class TunerTsStreamerManager {
     // The lock will protect mStreamerFinder, mSourceToStreamerMap and some part of TsStreamCreator
     // to support timely {@link TunerTsStreamer} cancellation due to a new tune request from
     // the same session.
     private final Object mCancelLock = new Object();
     private final StreamerFinder mStreamerFinder = new StreamerFinder();
     private final Map<Integer, TsStreamerCreator> mCreators = new HashMap<>();
-    private final Map<Integer, EventDetector.EventListener> mListeners = new HashMap<>();
+    private final Map<Integer, EventListener> mListeners = new HashMap<>();
     private final Map<TsDataSource, TunerTsStreamer> mSourceToStreamerMap = new HashMap<>();
-    private final TunerHalManager mTunerHalManager = new TunerHalManager();
-    private static TunerTsStreamerManager sInstance;
+    private final TunerHalManager mTunerHalManager;
 
-    /**
-     * Returns the singleton instance for the class
-     *
-     * @return TunerTsStreamerManager
-     */
-    static synchronized TunerTsStreamerManager getInstance() {
-        if (sInstance == null) {
-            sInstance = new TunerTsStreamerManager();
-        }
-        return sInstance;
+    @Inject
+    @VisibleForTesting
+    public TunerTsStreamerManager(TunerFactory tunerFactory) {
+        mTunerHalManager = new TunerHalManager(tunerFactory);
     }
 
-    private TunerTsStreamerManager() {}
-
     synchronized TsDataSource createDataSource(
             Context context,
             TunerChannel channel,
-            EventDetector.EventListener listener,
+            EventListener listener,
             int sessionId,
             boolean reuse) {
         TsStreamerCreator creator;
@@ -95,7 +92,7 @@
         }
         // Created streamer was cancelled by a new tune request.
         streamer.stopStream();
-        TunerHal hal = streamer.getTunerHal();
+        Tuner hal = streamer.getTunerHal();
         hal.setHasPendingTune(false);
         mTunerHalManager.releaseTunerHal(hal, sessionId, reuse);
         return null;
@@ -109,7 +106,7 @@
             if (streamer == null) {
                 return;
             }
-            EventDetector.EventListener listener = mListeners.remove(sessionId);
+            EventListener listener = mListeners.remove(sessionId);
             streamer.unregisterListener(listener);
             TunerChannel channel = streamer.getChannel();
             SoftPreconditions.checkState(channel != null);
@@ -119,7 +116,7 @@
             }
         }
         streamer.stopStream();
-        TunerHal hal = streamer.getTunerHal();
+        Tuner hal = streamer.getTunerHal();
         hal.setHasPendingTune(false);
         mTunerHalManager.releaseTunerHal(hal, sessionId, reuse);
     }
@@ -133,7 +130,7 @@
     }
 
     /** Add tuner hal into TunerHalManager for test. */
-    void addTunerHal(TunerHal tunerHal, int sessionId) {
+    void addTunerHal(Tuner tunerHal, int sessionId) {
         mTunerHalManager.addTunerHal(tunerHal, sessionId);
     }
 
@@ -188,21 +185,20 @@
     private class TsStreamerCreator {
         private final Context mContext;
         private final TunerChannel mChannel;
-        private final EventDetector.EventListener mEventListener;
+        private final EventListener mEventListener;
         // mCancelled will be {@code true} if a new tune request for the same session
         // cancels create().
         private boolean mCancelled;
-        private TunerHal mTunerHal;
+        private Tuner mTunerHal;
 
-        private TsStreamerCreator(
-                Context context, TunerChannel channel, EventDetector.EventListener listener) {
+        private TsStreamerCreator(Context context, TunerChannel channel, EventListener listener) {
             mContext = context;
             mChannel = channel;
             mEventListener = listener;
         }
 
         private TunerTsStreamer create(int sessionId, boolean reuse) {
-            TunerHal hal = mTunerHalManager.getOrCreateTunerHal(mContext, sessionId);
+            Tuner hal = mTunerHalManager.getOrCreateTunerHal(mContext, sessionId);
             if (hal == null) {
                 return null;
             }
@@ -248,15 +244,20 @@
     }
 
     /**
-     * Supports sharing {@link TunerHal} among multiple sessions. The class also supports session
-     * affinity for {@link TunerHal} allocation.
+     * Supports sharing {@link Tuner} among multiple sessions. The class also supports session
+     * affinity for {@link Tuner} allocation.
      */
     private static class TunerHalManager {
-        private final Map<Integer, TunerHal> mTunerHals = new HashMap<>();
+        private final Map<Integer, Tuner> mTunerHals = new HashMap<>();
+        private final TunerFactory mTunerFactory;
 
-        private TunerHal getOrCreateTunerHal(Context context, int sessionId) {
+        private TunerHalManager(TunerFactory mTunerFactory) {
+            this.mTunerFactory = mTunerFactory;
+        }
+
+        private Tuner getOrCreateTunerHal(Context context, int sessionId) {
             // Handles session affinity.
-            TunerHal hal = mTunerHals.get(sessionId);
+            Tuner hal = mTunerHals.get(sessionId);
             if (hal != null) {
                 mTunerHals.remove(sessionId);
                 return hal;
@@ -269,15 +270,15 @@
                 mTunerHals.remove(key);
                 return hal;
             }
-            return TunerHal.createInstance(context);
+            return mTunerFactory.createInstance(context);
         }
 
-        private void releaseTunerHal(TunerHal hal, int sessionId, boolean reuse) {
+        private void releaseTunerHal(Tuner hal, int sessionId, boolean reuse) {
             if (!reuse || !hal.isReusable()) {
                 AutoCloseableUtils.closeQuietly(hal);
                 return;
             }
-            TunerHal cachedHal = mTunerHals.get(sessionId);
+            Tuner cachedHal = mTunerHals.get(sessionId);
             if (cachedHal != hal) {
                 mTunerHals.put(sessionId, hal);
                 if (cachedHal != null) {
@@ -287,7 +288,7 @@
         }
 
         private void releaseCachedHal(int sessionId) {
-            TunerHal hal = mTunerHals.get(sessionId);
+            Tuner hal = mTunerHals.get(sessionId);
             if (hal != null) {
                 mTunerHals.remove(sessionId);
             }
@@ -296,7 +297,7 @@
             }
         }
 
-        private void addTunerHal(TunerHal tunerHal, int sessionId) {
+        private void addTunerHal(Tuner tunerHal, int sessionId) {
             mTunerHals.put(sessionId, tunerHal);
         }
     }
diff --git a/tuner/src/com/android/tv/tuner/tvinput/EventDetector.java b/tuner/src/com/android/tv/tuner/ts/EventDetector.java
similarity index 92%
rename from tuner/src/com/android/tv/tuner/tvinput/EventDetector.java
rename to tuner/src/com/android/tv/tuner/ts/EventDetector.java
index c529c6d..6d1fc27 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/EventDetector.java
+++ b/tuner/src/com/android/tv/tuner/ts/EventDetector.java
@@ -14,18 +14,18 @@
  * limitations under the License.
  */
 
-package com.android.tv.tuner.tvinput;
+package com.android.tv.tuner.ts;
 
 import android.util.Log;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
-import com.android.tv.tuner.TunerHal;
+import com.android.tv.tuner.api.Tuner;
 import com.android.tv.tuner.data.PsiData;
 import com.android.tv.tuner.data.PsipData;
+import com.android.tv.tuner.data.PsipData.EitItem;
 import com.android.tv.tuner.data.TunerChannel;
 import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
 import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
-import com.android.tv.tuner.ts.TsParser;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -39,7 +39,7 @@
     private static final boolean DEBUG = false;
     public static final int ALL_PROGRAM_NUMBERS = -1;
 
-    private final TunerHal mTunerHal;
+    private final Tuner mTunerHal;
 
     private TsParser mTsParser;
     private final Set<Integer> mPidSet = new HashSet<>();
@@ -62,7 +62,7 @@
                     for (PsiData.PatItem i : items) {
                         if (mProgramNumber == ALL_PROGRAM_NUMBERS
                                 || mProgramNumber == i.getProgramNo()) {
-                            mTunerHal.addPidFilter(i.getPmtPid(), TunerHal.FILTER_TYPE_OTHER);
+                            mTunerHal.addPidFilter(i.getPmtPid(), Tuner.FILTER_TYPE_OTHER);
                         }
                     }
                 }
@@ -225,15 +225,7 @@
             };
 
     /** Listener for detecting ATSC TV channels and receiving EPG data. */
-    public interface EventListener {
-
-        /**
-         * Fired when new information of an ATSC TV channel arrived.
-         *
-         * @param channel an ATSC TV channel
-         * @param channelArrivedAtFirstTime tells whether this channel arrived at first time
-         */
-        void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime);
+    public interface EventListener extends com.android.tv.tuner.api.ChannelScanListener {
 
         /**
          * Fired when new program events of an ATSC TV channel arrived.
@@ -241,7 +233,7 @@
          * @param channel an ATSC TV channel
          * @param items a list of EIT items that were received
          */
-        void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items);
+        void onEventDetected(TunerChannel channel, List<EitItem> items);
 
         /**
          * Fired when information of all detectable ATSC TV channels in current frequency arrived.
@@ -250,21 +242,20 @@
     }
 
     /**
-     * Creates a detector for ATSC TV channles and program information.
+     * Creates a detector for ATSC TV channels and program information.
      *
-     * @param usbTunerInteface {@link TunerHal}
+     * @param tunerHal
      */
-    public EventDetector(TunerHal usbTunerInteface) {
-        mTunerHal = usbTunerInteface;
+    public EventDetector(Tuner tunerHal) {
+        mTunerHal = tunerHal;
     }
 
     private void reset() {
         // TODO: Use TsParser.reset()
-        int deliverySystemType = mTunerHal.getDeliverySystemType();
         mTsParser =
                 new TsParser(
                         mTsOutputListener,
-                        TunerHal.isDvbDeliverySystem(mTunerHal.getDeliverySystemType()));
+                        Tuner.isDvbDeliverySystem(mTunerHal.getDeliverySystemType()));
         mPidSet.clear();
         mVctProgramNumberSet.clear();
         mSdtProgramNumberSet.clear();
@@ -293,7 +284,7 @@
             return;
         }
         mPidSet.add(pid);
-        mTunerHal.addPidFilter(pid, TunerHal.FILTER_TYPE_OTHER);
+        mTunerHal.addPidFilter(pid, Tuner.FILTER_TYPE_OTHER);
     }
 
     /**
diff --git a/tuner/src/com/android/tv/tuner/ts/TsParser.java b/tuner/src/com/android/tv/tuner/ts/TsParser.java
index 2307c22..be46983 100644
--- a/tuner/src/com/android/tv/tuner/ts/TsParser.java
+++ b/tuner/src/com/android/tv/tuner/ts/TsParser.java
@@ -26,8 +26,9 @@
 import com.android.tv.tuner.data.PsipData.MgtItem;
 import com.android.tv.tuner.data.PsipData.SdtItem;
 import com.android.tv.tuner.data.PsipData.VctItem;
+import com.android.tv.tuner.data.SectionParser;
+import com.android.tv.tuner.data.SectionParser.OutputListener;
 import com.android.tv.tuner.data.TunerChannel;
-import com.android.tv.tuner.ts.SectionParser.OutputListener;
 import com.android.tv.tuner.util.ByteArrayBuffer;
 import java.util.ArrayList;
 import java.util.Arrays;
diff --git a/tuner/src/com/android/tv/tuner/tvinput/AudioCapabilitiesReceiverV1Wrapper.java b/tuner/src/com/android/tv/tuner/tvinput/AudioCapabilitiesReceiverV1Wrapper.java
new file mode 100644
index 0000000..4c35ea4
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/tvinput/AudioCapabilitiesReceiverV1Wrapper.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.tvinput;
+
+import android.content.Context;
+import android.os.Handler;
+import android.util.Log;
+import com.google.android.exoplayer.audio.AudioCapabilities;
+import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver;
+
+/**
+ * Wraps {@link AudioCapabilitiesReceiver} to support listening for audio capabilities changes on
+ * custom threads.
+ */
+public final class AudioCapabilitiesReceiverV1Wrapper {
+
+    private static final String TAG = "AudioCapabilitiesReceiverV1Wrapper";
+
+    private final AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
+    private final Handler mHandler;
+    private final AudioCapabilitiesReceiver.Listener mListener;
+    private boolean mRegistered;
+
+    /**
+     * Creates an instance.
+     *
+     * @param context A context for registering the receiver.
+     * @param handler A handler on the which mListener events will be posted.
+     * @param listener The listener to notify when audio capabilities change.
+     */
+    public AudioCapabilitiesReceiverV1Wrapper(
+            Context context, Handler handler, AudioCapabilitiesReceiver.Listener listener) {
+        mAudioCapabilitiesReceiver =
+                new AudioCapabilitiesReceiver(context, this::onAudioCapabilitiesChanged);
+        mHandler = handler;
+        mListener = listener;
+    }
+
+    /** @see AudioCapabilitiesReceiver#register() */
+    public AudioCapabilities register() {
+        mRegistered = true;
+        return mAudioCapabilitiesReceiver.register();
+    }
+
+    /** @see AudioCapabilitiesReceiver#unregister() */
+    public void unregister() {
+        if (mRegistered) {
+            try {
+                mAudioCapabilitiesReceiver.unregister();
+            } catch (IllegalArgumentException e) {
+                // Workaround for b/115739362.
+                Log.e(
+                        TAG,
+                        "Ignoring exception when unregistering audio capabilities receiver: ",
+                        e);
+            }
+            mRegistered = false;
+        } else {
+            Log.e(TAG, "Attempt to unregister a non-registered audio capabilities receiver.");
+        }
+    }
+
+    private void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) {
+        mHandler.post(() -> mListener.onAudioCapabilitiesChanged(audioCapabilities));
+    }
+}
diff --git a/tuner/src/com/android/tv/tuner/tvinput/BaseTunerTvInputService.java b/tuner/src/com/android/tv/tuner/tvinput/BaseTunerTvInputService.java
index e577e35..d22b639 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/BaseTunerTvInputService.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/BaseTunerTvInputService.java
@@ -23,26 +23,29 @@
 import android.media.tv.TvInputService;
 import android.util.Log;
 import com.android.tv.common.feature.CommonFeatures;
-import com.google.android.exoplayer.audio.AudioCapabilities;
-import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver;
+import com.android.tv.tuner.source.TsDataSourceManager;
+import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactory;
+import dagger.android.AndroidInjection;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
 import java.util.Collections;
 import java.util.Set;
 import java.util.WeakHashMap;
 import java.util.concurrent.TimeUnit;
+import javax.inject.Inject;
 
 /** {@link BaseTunerTvInputService} serves TV channels coming from a tuner device. */
-public class BaseTunerTvInputService extends TvInputService
-        implements AudioCapabilitiesReceiver.Listener {
+public class BaseTunerTvInputService extends TvInputService {
     private static final String TAG = "BaseTunerTvInputService";
     private static final boolean DEBUG = false;
 
     private static final int DVR_STORAGE_CLEANUP_JOB_ID = 100;
 
-    // WeakContainer for {@link TvInputSessionImpl}
-    private final Set<TunerSession> mTunerSessions = Collections.newSetFromMap(new WeakHashMap<>());
+    private final Set<Session> mTunerSessions = Collections.newSetFromMap(new WeakHashMap<>());
     private ChannelDataManager mChannelDataManager;
-    private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
-    private AudioCapabilities mAudioCapabilities;
+    @Inject ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
+    @Inject TsDataSourceManager.Factory mTsDataSourceManagerFactory;
+    @Inject TunerSessionFactory mTunerSessionFactory;
 
     @Override
     public void onCreate() {
@@ -51,11 +54,10 @@
             this.stopSelf();
             return;
         }
+        AndroidInjection.inject(this);
         super.onCreate();
         if (DEBUG) Log.d(TAG, "onCreate");
         mChannelDataManager = new ChannelDataManager(getApplicationContext());
-        mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(getApplicationContext(), this);
-        mAudioCapabilitiesReceiver.register();
         if (CommonFeatures.DVR.isEnabled(this)) {
             JobScheduler jobScheduler =
                     (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
@@ -80,12 +82,16 @@
         if (DEBUG) Log.d(TAG, "onDestroy");
         super.onDestroy();
         mChannelDataManager.release();
-        mAudioCapabilitiesReceiver.unregister();
     }
 
     @Override
     public RecordingSession onCreateRecordingSession(String inputId) {
-        return new TunerRecordingSession(this, inputId, mChannelDataManager);
+        return new TunerRecordingSession(
+                this,
+                inputId,
+                mChannelDataManager,
+                mConcurrentDvrPlaybackFlags,
+                mTsDataSourceManagerFactory);
     }
 
     @Override
@@ -93,13 +99,13 @@
         if (DEBUG) Log.d(TAG, "onCreateSession");
         try {
             // TODO(b/65445352): Support multiple TunerSessions for multiple tuners
-            if (!allSessionsReleased()) {
+            if (!mTunerSessions.isEmpty()) {
                 Log.d(TAG, "abort creating an session");
                 return null;
             }
-            final TunerSession session = new TunerSession(this, mChannelDataManager);
+            final Session session =
+                    mTunerSessionFactory.create(this, mChannelDataManager, this::onReleased);
             mTunerSessions.add(session);
-            session.setAudioCapabilities(mAudioCapabilities);
             session.setOverlayViewEnabled(true);
             return session;
         } catch (RuntimeException e) {
@@ -109,22 +115,7 @@
         }
     }
 
-    @Override
-    public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) {
-        mAudioCapabilities = audioCapabilities;
-        for (TunerSession session : mTunerSessions) {
-            if (!session.isReleased()) {
-                session.setAudioCapabilities(audioCapabilities);
-            }
-        }
-    }
-
-    private boolean allSessionsReleased() {
-        for (TunerSession session : mTunerSessions) {
-            if (!session.isReleased()) {
-                return false;
-            }
-        }
-        return true;
+    private void onReleased(Session session) {
+        mTunerSessions.remove(session);
     }
 }
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java b/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java
index a1f0c77..5561693 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java
@@ -17,25 +17,38 @@
 package com.android.tv.tuner.tvinput;
 
 import android.content.Context;
-import android.media.tv.TvInputService;
 import android.net.Uri;
 import android.support.annotation.MainThread;
 import android.support.annotation.Nullable;
 import android.support.annotation.WorkerThread;
 import android.util.Log;
+import com.android.tv.common.compat.RecordingSessionCompat;
+import com.android.tv.tuner.source.TsDataSourceManager;
+import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
 
 /** Processes DVR recordings, and deletes the previously recorded contents. */
-public class TunerRecordingSession extends TvInputService.RecordingSession {
+public class TunerRecordingSession extends RecordingSessionCompat {
     private static final String TAG = "TunerRecordingSession";
     private static final boolean DEBUG = false;
 
     private final TunerRecordingSessionWorker mSessionWorker;
 
     public TunerRecordingSession(
-            Context context, String inputId, ChannelDataManager channelDataManager) {
+            Context context,
+            String inputId,
+            ChannelDataManager channelDataManager,
+            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
+            TsDataSourceManager.Factory tsDataSourceManagerFactory) {
         super(context);
         mSessionWorker =
-                new TunerRecordingSessionWorker(context, inputId, channelDataManager, this);
+                new TunerRecordingSessionWorker(
+                        context,
+                        inputId,
+                        channelDataManager,
+                        this,
+                        concurrentDvrPlaybackFlags,
+                        tsDataSourceManagerFactory);
     }
 
     // RecordingSession
@@ -85,6 +98,15 @@
         notifyTuned(channelUri);
     }
 
+    // Called from TunerRecordingSessionImpl in a worker thread.
+    @WorkerThread
+    public void onRecordingUri(String recUri) {
+        if (DEBUG) {
+            Log.d(TAG, "Notifying recording session URI." + recUri);
+        }
+        notifyRecordingStarted(recUri);
+    }
+
     @WorkerThread
     public void onRecordFinished(final Uri recordedProgramUri) {
         if (DEBUG) {
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java b/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java
index b200122..2c0c09a 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java
@@ -16,6 +16,8 @@
 
 package com.android.tv.tuner.tvinput;
 
+import static com.android.tv.tuner.features.TunerFeatures.TVPROVIDER_ALLOWS_COLUMN_CREATION;
+
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
@@ -26,16 +28,18 @@
 import android.media.tv.TvInputManager;
 import android.net.Uri;
 import android.os.AsyncTask;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Message;
 import android.support.annotation.IntDef;
 import android.support.annotation.MainThread;
 import android.support.annotation.Nullable;
-import android.support.media.tv.Program;
 import android.util.Log;
 import android.util.Pair;
+import androidx.tvprovider.media.tv.Program;
 import com.android.tv.common.BaseApplication;
+import com.android.tv.common.data.RecordedProgramState;
 import com.android.tv.common.recording.RecordingCapability;
 import com.android.tv.common.recording.RecordingStorageStatusManager;
 import com.android.tv.common.util.CommonUtils;
@@ -48,23 +52,31 @@
 import com.android.tv.tuner.exoplayer.SampleExtractor;
 import com.android.tv.tuner.exoplayer.buffer.BufferManager;
 import com.android.tv.tuner.exoplayer.buffer.DvrStorageManager;
+import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener;
 import com.android.tv.tuner.source.TsDataSource;
 import com.android.tv.tuner.source.TsDataSourceManager;
+import com.android.tv.tuner.ts.EventDetector.EventListener;
+import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
 import com.google.android.exoplayer.C;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
 import java.io.File;
 import java.io.IOException;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Random;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
 /** Implements a DVR feature. */
 public class TunerRecordingSessionWorker
         implements PlaybackBufferListener,
-                EventDetector.EventListener,
+                EventListener,
                 SampleExtractor.OnCompletionListener,
                 Handler.Callback {
     private static final String TAG = "TunerRecordingSessionW";
@@ -87,6 +99,14 @@
     private static final int MSG_MONITOR_STORAGE_STATUS = 5;
     private static final int MSG_RELEASE = 6;
     private static final int MSG_UPDATE_CC_INFO = 7;
+    private static final int MSG_UPDATE_PARTIAL_STATE = 8;
+    private static final String COLUMN_SERIES_ID = "series_id";
+    private static final String COLUMN_STATE = "state";
+
+    private boolean mProgramHasSeriesIdColumn;
+    private boolean mRecordedProgramHasSeriesIdColumn;
+    private boolean mRecordedProgramHasStateColumn;
+
     private final RecordingCapability mCapabilities;
 
     private static final String[] PROGRAM_PROJECTION = {
@@ -108,6 +128,9 @@
         TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA
     };
 
+    private static final String[] PROGRAM_PROJECTION_WITH_SERIES_ID =
+            createProjectionWithSeriesId();
+
     @IntDef({STATE_IDLE, STATE_TUNING, STATE_TUNED, STATE_RECORDING})
     @Retention(RetentionPolicy.SOURCE)
     public @interface DvrSessionState {}
@@ -119,6 +142,7 @@
 
     private static final long CHANNEL_ID_NONE = -1;
     private static final int MAX_TUNING_RETRY = 6;
+    private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
 
     private final Context mContext;
     private final ChannelDataManager mChannelDataManager;
@@ -132,12 +156,14 @@
     private File mStorageDir;
     private long mRecordStartTime;
     private long mRecordEndTime;
+    private Uri mRecordedProgramUri;
     private boolean mRecorderRunning;
     private SampleExtractor mRecorder;
     private final TunerRecordingSession mSession;
     @DvrSessionState private int mSessionState = STATE_IDLE;
     private final String mInputId;
     private Uri mProgramUri;
+    private String mSeriesId;
 
     private PsipData.EitItem mCurrenProgram;
     private List<AtscCaptionTrack> mCaptionTracks;
@@ -147,7 +173,10 @@
             Context context,
             String inputId,
             ChannelDataManager dataManager,
-            TunerRecordingSession session) {
+            TunerRecordingSession session,
+            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
+            TsDataSourceManager.Factory tsDataSourceManagerFactory) {
+        mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
         mRandom.setSeed(System.nanoTime());
         mContext = context;
         HandlerThread handlerThread = new HandlerThread(TAG);
@@ -157,7 +186,7 @@
                 BaseApplication.getSingletons(context).getRecordingStorageStatusManager();
         mChannelDataManager = dataManager;
         mChannelDataManager.checkDataVersion(context);
-        mSourceManager = TsDataSourceManager.createSourceManager(true);
+        mSourceManager = tsDataSourceManagerFactory.create(true);
         mCapabilities = new DvbDeviceAccessor(context).getRecordingCapability(inputId);
         mInputId = inputId;
         if (DEBUG) Log.d(TAG, mCapabilities.toString());
@@ -306,6 +335,7 @@
                         }
                         new DeleteRecordingTask().execute(mStorageDir);
                         mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
+                        mContext.getContentResolver().delete(mRecordedProgramUri, null, null);
                         reset();
                     } else {
                         mHandler.sendEmptyMessageDelayed(
@@ -330,6 +360,11 @@
                     updateCaptionTracks(pair.first, pair.second);
                     return true;
                 }
+            case MSG_UPDATE_PARTIAL_STATE:
+                {
+                    updateRecordedProgram(RecordedProgramState.PARTIAL, -1, -1);
+                    return true;
+                }
         }
         return false;
     }
@@ -422,17 +457,46 @@
         mDvrStorageManager = new DvrStorageManager(mStorageDir, true);
         mRecorder =
                 new ExoPlayerSampleExtractor(
-                        Uri.EMPTY, mTunerSource, new BufferManager(mDvrStorageManager), this, true);
+                        Uri.EMPTY,
+                        mTunerSource,
+                        new BufferManager(mDvrStorageManager),
+                        this,
+                        true,
+                        mConcurrentDvrPlaybackFlags);
         mRecorder.setOnCompletionListener(this, mHandler);
         mProgramUri = programUri;
         mSessionState = STATE_RECORDING;
         mRecorderRunning = true;
+        if (mConcurrentDvrPlaybackFlags.enabled()) {
+            mRecordedProgramUri =
+                    insertRecordedProgram(
+                            getRecordedProgram(),
+                            mChannel.getChannelId(),
+                            Uri.fromFile(mStorageDir).toString(),
+                            calculateRecordingSizeInBytes(),
+                            mRecordStartTime,
+                            mRecordStartTime);
+            if (mRecordedProgramUri == null) {
+                new DeleteRecordingTask().execute(mStorageDir);
+                mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
+                Log.e(TAG, "Inserting a recording to DB failed");
+                return false;
+            }
+            mSession.onRecordingUri(mRecordedProgramUri.toString());
+            mHandler.sendEmptyMessageDelayed(
+                    MSG_UPDATE_PARTIAL_STATE, MIN_PARTIAL_RECORDING_DURATION_MS);
+        }
         mHandler.sendEmptyMessage(MSG_PREPARE_RECODER);
         mHandler.removeMessages(MSG_MONITOR_STORAGE_STATUS);
         mHandler.sendEmptyMessageDelayed(MSG_MONITOR_STORAGE_STATUS, STORAGE_MONITOR_INTERVAL_MS);
         return true;
     }
 
+    private int calculateRecordingSizeInBytes() {
+        // TODO(b/121153491): calcute recording size using mStorageDir
+        return 1024 * 1024;
+    }
+
     private void stopRecorder() {
         // Do not change session status.
         if (mRecorder != null) {
@@ -485,9 +549,15 @@
             long avg = mRecordStartTime / 2 + mRecordEndTime / 2;
             programUri = TvContract.buildProgramsUriForChannel(mChannel.getChannelId(), avg, avg);
         }
-        try (Cursor c = resolver.query(programUri, PROGRAM_PROJECTION, null, null, SORT_BY_TIME)) {
+        String[] projection =
+                checkProgramTable() ? PROGRAM_PROJECTION_WITH_SERIES_ID : PROGRAM_PROJECTION;
+        try (Cursor c = resolver.query(programUri, projection, null, null, SORT_BY_TIME)) {
             if (c != null && c.moveToNext()) {
                 Program result = Program.fromCursor(c);
+                int index;
+                if ((index = c.getColumnIndex(COLUMN_SERIES_ID)) >= 0 && !c.isNull(index)) {
+                    mSeriesId = c.getString(index);
+                }
                 if (DEBUG) {
                     Log.v(TAG, "Finished query for " + this);
                 }
@@ -516,9 +586,15 @@
         values.put(RecordedPrograms.COLUMN_RECORDING_DATA_URI, storageUri);
         values.put(RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, endTime - startTime);
         values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, totalBytes);
-        // startTime and endTime could be overridden by program's start and end value.
+        // startTime could be overridden by program's start value.
         values.put(RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, startTime);
         values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, endTime);
+        if (checkRecordedProgramTable(COLUMN_SERIES_ID)) {
+            values.put(COLUMN_SERIES_ID, mSeriesId);
+        }
+        if (mConcurrentDvrPlaybackFlags.enabled() && checkRecordedProgramTable(COLUMN_STATE)) {
+            values.put(COLUMN_STATE, RecordedProgramState.STARTED.name());
+        }
         if (program != null) {
             values.putAll(program.toContentValues());
         }
@@ -526,6 +602,20 @@
                 .insert(TvContract.RecordedPrograms.CONTENT_URI, values);
     }
 
+    private void updateRecordedProgram(RecordedProgramState state, long endTime, long totalBytes) {
+        ContentValues values = new ContentValues();
+        if (checkRecordedProgramTable(COLUMN_STATE)) {
+            values.put(COLUMN_STATE, state.name());
+        }
+        if (state.equals(RecordedProgramState.FINISHED)) {
+            values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, totalBytes);
+            values.put(
+                    RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, endTime - mRecordStartTime);
+            values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, endTime);
+        }
+        mContext.getContentResolver().update(mRecordedProgramUri, values, null, null);
+    }
+
     private void onRecordingResult(boolean success, long lastExtractedPositionUs) {
         if (mSessionState != STATE_RECORDING) {
             // Error notification is not needed.
@@ -541,6 +631,7 @@
                         < TimeUnit.MILLISECONDS.toMicros(MIN_PARTIAL_RECORDING_DURATION_MS)) {
             new DeleteRecordingTask().execute(mStorageDir);
             mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
+            mContext.getContentResolver().delete(mRecordedProgramUri, null, null);
             Log.w(TAG, "Recording failed during recording");
             return;
         }
@@ -549,22 +640,120 @@
                 (lastExtractedPositionUs == C.UNKNOWN_TIME_US)
                         ? System.currentTimeMillis()
                         : mRecordStartTime + lastExtractedPositionUs / 1000;
-        Uri uri =
-                insertRecordedProgram(
-                        getRecordedProgram(),
-                        mChannel.getChannelId(),
-                        Uri.fromFile(mStorageDir).toString(),
-                        1024 * 1024,
-                        mRecordStartTime,
-                        recordEndTime);
-        if (uri == null) {
-            new DeleteRecordingTask().execute(mStorageDir);
-            mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
-            Log.e(TAG, "Inserting a recording to DB failed");
-            return;
+        if (!mConcurrentDvrPlaybackFlags.enabled()) {
+            mRecordedProgramUri =
+                    insertRecordedProgram(
+                            getRecordedProgram(),
+                            mChannel.getChannelId(),
+                            Uri.fromFile(mStorageDir).toString(),
+                            calculateRecordingSizeInBytes(),
+                            mRecordStartTime,
+                            recordEndTime);
+            if (mRecordedProgramUri == null) {
+                new DeleteRecordingTask().execute(mStorageDir);
+                mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
+                Log.e(TAG, "Inserting a recording to DB failed");
+                return;
+            }
+        } else {
+            updateRecordedProgram(
+                    RecordedProgramState.FINISHED, recordEndTime, calculateRecordingSizeInBytes());
         }
         mDvrStorageManager.writeCaptionInfoFiles(mCaptionTracks);
-        mSession.onRecordFinished(uri);
+        mSession.onRecordFinished(mRecordedProgramUri);
+    }
+
+    private boolean checkProgramTable() {
+        boolean canCreateColumn = TVPROVIDER_ALLOWS_COLUMN_CREATION.isEnabled(mContext);
+        if (!canCreateColumn) {
+            return false;
+        }
+        Uri uri = TvContract.Programs.CONTENT_URI;
+        if (!mProgramHasSeriesIdColumn) {
+            if (getExistingColumns(uri).contains(COLUMN_SERIES_ID)) {
+                mProgramHasSeriesIdColumn = true;
+            } else if (addColumnToTable(uri, COLUMN_SERIES_ID)) {
+                mProgramHasSeriesIdColumn = true;
+            }
+        }
+        return mProgramHasSeriesIdColumn;
+    }
+
+    private boolean checkRecordedProgramTable(String column) {
+        boolean canCreateColumn = TVPROVIDER_ALLOWS_COLUMN_CREATION.isEnabled(mContext);
+        if (!canCreateColumn) {
+            return false;
+        }
+        Uri uri = TvContract.RecordedPrograms.CONTENT_URI;
+        switch (column) {
+            case COLUMN_SERIES_ID:
+                {
+                    if (!mRecordedProgramHasSeriesIdColumn) {
+                        if (getExistingColumns(uri).contains(COLUMN_SERIES_ID)) {
+                            mRecordedProgramHasSeriesIdColumn = true;
+                        } else if (addColumnToTable(uri, COLUMN_SERIES_ID)) {
+                            mRecordedProgramHasSeriesIdColumn = true;
+                        }
+                    }
+                    return mRecordedProgramHasSeriesIdColumn;
+                }
+            case COLUMN_STATE:
+                {
+                    if (!mRecordedProgramHasStateColumn) {
+                        if (getExistingColumns(uri).contains(COLUMN_STATE)) {
+                            mRecordedProgramHasStateColumn = true;
+                        } else if (addColumnToTable(uri, COLUMN_STATE)) {
+                            mRecordedProgramHasStateColumn = true;
+                        }
+                    }
+                    return mRecordedProgramHasStateColumn;
+                }
+            default:
+                return false;
+        }
+    }
+
+    private Set<String> getExistingColumns(Uri uri) {
+        Bundle result =
+                mContext.getContentResolver()
+                        .call(uri, TvContract.METHOD_GET_COLUMNS, uri.toString(), null);
+        if (result != null) {
+            String[] columns = result.getStringArray(TvContract.EXTRA_EXISTING_COLUMN_NAMES);
+            if (columns != null) {
+                return new HashSet<>(Arrays.asList(columns));
+            }
+        }
+        Log.e(TAG, "Query existing column names from " + uri + " returned null");
+        return Collections.emptySet();
+    }
+
+    /**
+     * Add a column to the table
+     *
+     * @return {@code true} if the column is added successfully; {@code false} otherwise.
+     */
+    private boolean addColumnToTable(Uri contentUri, String columnName) {
+        Bundle extra = new Bundle();
+        extra.putCharSequence(TvContract.EXTRA_COLUMN_NAME, columnName);
+        extra.putCharSequence(TvContract.EXTRA_DATA_TYPE, "TEXT");
+        // If the add operation fails, the following just returns null without crashing.
+        Bundle allColumns =
+                mContext.getContentResolver()
+                        .call(
+                                contentUri,
+                                TvContract.METHOD_ADD_COLUMN,
+                                contentUri.toString(),
+                                extra);
+        if (allColumns == null) {
+            Log.w(TAG, "Adding new column failed. Uri=" + contentUri);
+        }
+        return allColumns != null;
+    }
+
+    private static String[] createProjectionWithSeriesId() {
+        List<String> projectionList = new ArrayList<>(Arrays.asList(PROGRAM_PROJECTION));
+        projectionList.add(COLUMN_SERIES_ID);
+        return projectionList.toArray(new String[0]);
     }
 
     private static class DeleteRecordingTask extends AsyncTask<File, Void, Void> {
@@ -575,7 +764,9 @@
                 return null;
             }
             for (File file : files) {
-                CommonUtils.deleteDirOrFile(file);
+                if (!CommonUtils.deleteDirOrFile(file)) {
+                    Log.w(TAG, "Unable to delete recording data at " + file);
+                }
             }
             return null;
         }
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerSession.java b/tuner/src/com/android/tv/tuner/tvinput/TunerSession.java
index c9d997f..fedb5f6 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/TunerSession.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/TunerSession.java
@@ -21,95 +21,58 @@
 import android.media.PlaybackParams;
 import android.media.tv.TvContentRating;
 import android.media.tv.TvInputManager;
-import android.media.tv.TvInputService;
 import android.net.Uri;
 import android.os.Build;
-import android.os.Handler;
-import android.os.Message;
 import android.os.SystemClock;
-import android.text.Html;
 import android.util.Log;
-import android.view.LayoutInflater;
 import android.view.Surface;
 import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-import android.widget.Toast;
 import com.android.tv.common.CommonPreferences.CommonPreferencesChangedListener;
-import com.android.tv.common.util.SystemPropertiesProxy;
-import com.android.tv.tuner.R;
-import com.android.tv.tuner.TunerPreferences;
-import com.android.tv.tuner.cc.CaptionLayout;
-import com.android.tv.tuner.cc.CaptionTrackRenderer;
-import com.android.tv.tuner.data.Cea708Data.CaptionEvent;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
-import com.android.tv.tuner.util.GlobalSettingsUtils;
-import com.android.tv.tuner.util.StatusTextUtils;
-import com.google.android.exoplayer.audio.AudioCapabilities;
+import com.android.tv.common.compat.TisSessionCompat;
+import com.android.tv.tuner.prefs.TunerPreferences;
+import com.android.tv.tuner.source.TsDataSourceManager;
+import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactory.SessionReleasedCallback;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
 
 /**
- * Provides a tuner TV input session. It handles Overlay UI works. Main tuner input functions are
- * implemented in {@link TunerSessionWorker}.
+ * Provides a tuner TV input session. Main tuner input functions are implemented in {@link
+ * TunerSessionWorker}.
  */
-public class TunerSession extends TvInputService.Session
-        implements Handler.Callback, CommonPreferencesChangedListener {
+public class TunerSession extends TisSessionCompat implements CommonPreferencesChangedListener {
+
     private static final String TAG = "TunerSession";
     private static final boolean DEBUG = false;
-    private static final String USBTUNER_SHOW_DEBUG = "persist.tv.tuner.show_debug";
 
-    public static final int MSG_UI_SHOW_MESSAGE = 1;
-    public static final int MSG_UI_HIDE_MESSAGE = 2;
-    public static final int MSG_UI_SHOW_AUDIO_UNPLAYABLE = 3;
-    public static final int MSG_UI_HIDE_AUDIO_UNPLAYABLE = 4;
-    public static final int MSG_UI_PROCESS_CAPTION_TRACK = 5;
-    public static final int MSG_UI_START_CAPTION_TRACK = 6;
-    public static final int MSG_UI_STOP_CAPTION_TRACK = 7;
-    public static final int MSG_UI_RESET_CAPTION_TRACK = 8;
-    public static final int MSG_UI_CLEAR_CAPTION_RENDERER = 9;
-    public static final int MSG_UI_SET_STATUS_TEXT = 10;
-    public static final int MSG_UI_TOAST_RESCAN_NEEDED = 11;
-
-    private final Context mContext;
-    private final Handler mUiHandler;
-    private final View mOverlayView;
-    private final TextView mMessageView;
-    private final TextView mStatusView;
-    private final TextView mAudioStatusView;
-    private final ViewGroup mMessageLayout;
-    private final CaptionTrackRenderer mCaptionTrackRenderer;
+    private final TunerSessionOverlay mTunerSessionOverlay;
     private final TunerSessionWorker mSessionWorker;
-    private boolean mReleased = false;
+    private final SessionReleasedCallback mReleasedCallback;
     private boolean mPlayPaused;
     private long mTuneStartTimestamp;
 
-    public TunerSession(Context context, ChannelDataManager channelDataManager) {
+    public TunerSession(
+            Context context,
+            ChannelDataManager channelDataManager,
+            SessionReleasedCallback releasedCallback,
+            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
+            TsDataSourceManager.Factory tsDataSourceManagerFactory) {
         super(context);
-        mContext = context;
-        mUiHandler = new Handler(this);
-        LayoutInflater inflater =
-                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-        mOverlayView = inflater.inflate(R.layout.ut_overlay_view, null);
-        mMessageLayout = (ViewGroup) mOverlayView.findViewById(R.id.message_layout);
-        mMessageLayout.setVisibility(View.INVISIBLE);
-        mMessageView = (TextView) mOverlayView.findViewById(R.id.message);
-        mStatusView = (TextView) mOverlayView.findViewById(R.id.tuner_status);
-        boolean showDebug = SystemPropertiesProxy.getBoolean(USBTUNER_SHOW_DEBUG, false);
-        mStatusView.setVisibility(showDebug ? View.VISIBLE : View.INVISIBLE);
-        mAudioStatusView = (TextView) mOverlayView.findViewById(R.id.audio_status);
-        mAudioStatusView.setVisibility(View.INVISIBLE);
-        CaptionLayout captionLayout = (CaptionLayout) mOverlayView.findViewById(R.id.caption);
-        mCaptionTrackRenderer = new CaptionTrackRenderer(captionLayout);
-        mSessionWorker = new TunerSessionWorker(context, channelDataManager, this);
+        mReleasedCallback = releasedCallback;
+        mTunerSessionOverlay = new TunerSessionOverlay(context);
+        mSessionWorker =
+                new TunerSessionWorker(
+                        context,
+                        channelDataManager,
+                        this,
+                        mTunerSessionOverlay,
+                        concurrentDvrPlaybackFlags,
+                        tsDataSourceManagerFactory);
         TunerPreferences.setCommonPreferencesChangedListener(this);
     }
 
-    public boolean isReleased() {
-        return mReleased;
-    }
-
     @Override
     public View onCreateOverlayView() {
-        return mOverlayView;
+        return mTunerSessionOverlay.getOverlayView();
     }
 
     @Override
@@ -207,16 +170,12 @@
         if (DEBUG) {
             Log.d(TAG, "onRelease");
         }
-        mReleased = true;
+        // The session worker needs to be released before the overlay to ensure no messages are
+        // added by the worker after releasing the overlay.
         mSessionWorker.release();
-        mUiHandler.removeCallbacksAndMessages(null);
+        mTunerSessionOverlay.release();
         TunerPreferences.setCommonPreferencesChangedListener(null);
-    }
-
-    /** Sets {@link AudioCapabilities}. */
-    public void setAudioCapabilities(AudioCapabilities audioCapabilities) {
-        mSessionWorker.sendMessage(
-                TunerSessionWorker.MSG_AUDIO_CAPABILITIES_CHANGED, audioCapabilities);
+        mReleasedCallback.onReleased(this);
     }
 
     @Override
@@ -241,99 +200,6 @@
         }
     }
 
-    public void sendUiMessage(int message) {
-        mUiHandler.sendEmptyMessage(message);
-    }
-
-    public void sendUiMessage(int message, Object object) {
-        mUiHandler.obtainMessage(message, object).sendToTarget();
-    }
-
-    public void sendUiMessage(int message, int arg1, int arg2, Object object) {
-        mUiHandler.obtainMessage(message, arg1, arg2, object).sendToTarget();
-    }
-
-    @Override
-    public boolean handleMessage(Message msg) {
-        switch (msg.what) {
-            case MSG_UI_SHOW_MESSAGE:
-                {
-                    mMessageView.setText((String) msg.obj);
-                    mMessageLayout.setVisibility(View.VISIBLE);
-                    return true;
-                }
-            case MSG_UI_HIDE_MESSAGE:
-                {
-                    mMessageLayout.setVisibility(View.INVISIBLE);
-                    return true;
-                }
-            case MSG_UI_SHOW_AUDIO_UNPLAYABLE:
-                {
-                    // Showing message of enabling surround sound only when global surround sound
-                    // setting is "never".
-                    final int value =
-                            GlobalSettingsUtils.getEncodedSurroundOutputSettings(mContext);
-                    if (value == GlobalSettingsUtils.ENCODED_SURROUND_OUTPUT_NEVER) {
-                        mAudioStatusView.setText(
-                                Html.fromHtml(
-                                        StatusTextUtils.getAudioWarningInHTML(
-                                                mContext.getString(
-                                                        R.string.ut_surround_sound_disabled))));
-                    } else {
-                        mAudioStatusView.setText(
-                                Html.fromHtml(
-                                        StatusTextUtils.getAudioWarningInHTML(
-                                                mContext.getString(
-                                                        R.string
-                                                                .audio_passthrough_not_supported))));
-                    }
-                    mAudioStatusView.setVisibility(View.VISIBLE);
-                    return true;
-                }
-            case MSG_UI_HIDE_AUDIO_UNPLAYABLE:
-                {
-                    mAudioStatusView.setVisibility(View.INVISIBLE);
-                    return true;
-                }
-            case MSG_UI_PROCESS_CAPTION_TRACK:
-                {
-                    mCaptionTrackRenderer.processCaptionEvent((CaptionEvent) msg.obj);
-                    return true;
-                }
-            case MSG_UI_START_CAPTION_TRACK:
-                {
-                    mCaptionTrackRenderer.start((AtscCaptionTrack) msg.obj);
-                    return true;
-                }
-            case MSG_UI_STOP_CAPTION_TRACK:
-                {
-                    mCaptionTrackRenderer.stop();
-                    return true;
-                }
-            case MSG_UI_RESET_CAPTION_TRACK:
-                {
-                    mCaptionTrackRenderer.reset();
-                    return true;
-                }
-            case MSG_UI_CLEAR_CAPTION_RENDERER:
-                {
-                    mCaptionTrackRenderer.clear();
-                    return true;
-                }
-            case MSG_UI_SET_STATUS_TEXT:
-                {
-                    mStatusView.setText((CharSequence) msg.obj);
-                    return true;
-                }
-            case MSG_UI_TOAST_RESCAN_NEEDED:
-                {
-                    Toast.makeText(mContext, R.string.ut_rescan_needed, Toast.LENGTH_LONG).show();
-                    return true;
-                }
-        }
-        return false;
-    }
-
     @Override
     public void onCommonPreferencesChanged() {
         mSessionWorker.sendMessage(TunerSessionWorker.MSG_TUNER_PREFERENCES_CHANGED);
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerSessionExoV2.java b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionExoV2.java
new file mode 100644
index 0000000..4eca44d
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionExoV2.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.tvinput;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.media.PlaybackParams;
+import android.media.tv.TvContentRating;
+import android.media.tv.TvInputManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.Surface;
+import android.view.View;
+import com.android.tv.common.CommonPreferences.CommonPreferencesChangedListener;
+import com.android.tv.common.compat.TisSessionCompat;
+import com.android.tv.tuner.prefs.TunerPreferences;
+import com.android.tv.tuner.source.TsDataSourceManager;
+import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactory.SessionReleasedCallback;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+
+/** Provides a tuner TV input session. */
+public class TunerSessionExoV2 extends TisSessionCompat
+        implements CommonPreferencesChangedListener {
+
+    private static final String TAG = "TunerSessionExoV2";
+    private static final boolean DEBUG = false;
+
+    private final TunerSessionOverlay mTunerSessionOverlay;
+    private final TunerSessionWorkerExoV2 mSessionWorker;
+    private final SessionReleasedCallback mReleasedCallback;
+    private boolean mPlayPaused;
+    private long mTuneStartTimestamp;
+
+    public TunerSessionExoV2(
+            Context context,
+            ChannelDataManager channelDataManager,
+            SessionReleasedCallback releasedCallback,
+            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
+            TsDataSourceManager.Factory tsDataSourceManagerFactory) {
+        super(context);
+        mReleasedCallback = releasedCallback;
+        mTunerSessionOverlay = new TunerSessionOverlay(context);
+        mSessionWorker =
+                new TunerSessionWorkerExoV2(
+                        context,
+                        channelDataManager,
+                        this,
+                        mTunerSessionOverlay,
+                        concurrentDvrPlaybackFlags,
+                        tsDataSourceManagerFactory);
+        TunerPreferences.setCommonPreferencesChangedListener(this);
+    }
+
+    @Override
+    public View onCreateOverlayView() {
+        return mTunerSessionOverlay.getOverlayView();
+    }
+
+    @Override
+    public boolean onSelectTrack(int type, String trackId) {
+        mSessionWorker.sendMessage(TunerSessionWorkerExoV2.MSG_SELECT_TRACK, type, 0, trackId);
+        return false;
+    }
+
+    @Override
+    public void onSetCaptionEnabled(boolean enabled) {
+        mSessionWorker.setCaptionEnabled(enabled);
+    }
+
+    @Override
+    public void onSetStreamVolume(float volume) {
+        mSessionWorker.setStreamVolume(volume);
+    }
+
+    @Override
+    public boolean onSetSurface(Surface surface) {
+        mSessionWorker.setSurface(surface);
+        return true;
+    }
+
+    @Override
+    public void onTimeShiftPause() {
+        mSessionWorker.sendMessage(TunerSessionWorkerExoV2.MSG_TIMESHIFT_PAUSE);
+        mPlayPaused = true;
+    }
+
+    @Override
+    public void onTimeShiftResume() {
+        mSessionWorker.sendMessage(TunerSessionWorkerExoV2.MSG_TIMESHIFT_RESUME);
+        mPlayPaused = false;
+    }
+
+    @Override
+    public void onTimeShiftSeekTo(long timeMs) {
+        if (DEBUG) Log.d(TAG, "Timeshift seekTo requested position: " + timeMs / 1000);
+        mSessionWorker.sendMessage(
+                TunerSessionWorkerExoV2.MSG_TIMESHIFT_SEEK_TO, mPlayPaused ? 1 : 0, 0, timeMs);
+    }
+
+    @Override
+    public void onTimeShiftSetPlaybackParams(PlaybackParams params) {
+        mSessionWorker.sendMessage(
+                TunerSessionWorkerExoV2.MSG_TIMESHIFT_SET_PLAYBACKPARAMS, params);
+    }
+
+    @Override
+    public long onTimeShiftGetStartPosition() {
+        return mSessionWorker.getStartPosition();
+    }
+
+    @Override
+    public long onTimeShiftGetCurrentPosition() {
+        return mSessionWorker.getCurrentPosition();
+    }
+
+    @Override
+    public boolean onTune(Uri channelUri) {
+        if (DEBUG) {
+            Log.d(TAG, "onTune to " + channelUri != null ? channelUri.toString() : "");
+        }
+        if (channelUri == null) {
+            Log.w(TAG, "onTune() is failed due to null channelUri.");
+            mSessionWorker.stopTune();
+            return false;
+        }
+        mTuneStartTimestamp = SystemClock.elapsedRealtime();
+        mSessionWorker.tune(channelUri);
+        mPlayPaused = false;
+        return true;
+    }
+
+    @TargetApi(Build.VERSION_CODES.N)
+    @Override
+    public void onTimeShiftPlay(Uri recordUri) {
+        if (recordUri == null) {
+            Log.w(TAG, "onTimeShiftPlay() is failed due to null channelUri.");
+            mSessionWorker.stopTune();
+            return;
+        }
+        mTuneStartTimestamp = SystemClock.elapsedRealtime();
+        mSessionWorker.tune(recordUri);
+        mPlayPaused = false;
+    }
+
+    @Override
+    public void onUnblockContent(TvContentRating unblockedRating) {
+        mSessionWorker.sendMessage(TunerSessionWorkerExoV2.MSG_UNBLOCKED_RATING, unblockedRating);
+    }
+
+    @Override
+    public void onRelease() {
+        if (DEBUG) {
+            Log.d(TAG, "onRelease");
+        }
+        // The session worker needs to be released before the overlay to ensure no messages are
+        // added by the worker after releasing the overlay.
+        mSessionWorker.release();
+        mTunerSessionOverlay.release();
+        TunerPreferences.setCommonPreferencesChangedListener(null);
+        mReleasedCallback.onReleased(this);
+    }
+
+    @Override
+    public void notifyVideoAvailable() {
+        super.notifyVideoAvailable();
+        if (mTuneStartTimestamp != 0) {
+            Log.i(
+                    TAG,
+                    "[Profiler] Video available in "
+                            + (SystemClock.elapsedRealtime() - mTuneStartTimestamp)
+                            + " ms");
+            mTuneStartTimestamp = 0;
+        }
+    }
+
+    @Override
+    public void notifyVideoUnavailable(int reason) {
+        super.notifyVideoUnavailable(reason);
+        if (reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING
+                && reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL) {
+            notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE);
+        }
+    }
+
+    @Override
+    public void onCommonPreferencesChanged() {
+        mSessionWorker.sendMessage(TunerSessionWorkerExoV2.MSG_TUNER_PREFERENCES_CHANGED);
+    }
+}
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerSessionOverlay.java b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionOverlay.java
new file mode 100644
index 0000000..9f21e16
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionOverlay.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.tvinput;
+
+import android.content.Context;
+import android.media.tv.TvInputService.Session;
+import android.os.Handler;
+import android.os.Message;
+import android.text.Html;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.android.tv.common.util.SystemPropertiesProxy;
+import com.android.tv.tuner.R;
+import com.android.tv.tuner.cc.CaptionLayout;
+import com.android.tv.tuner.cc.CaptionTrackRenderer;
+import com.android.tv.tuner.data.Cea708Data.CaptionEvent;
+import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.util.GlobalSettingsUtils;
+import com.android.tv.tuner.util.StatusTextUtils;
+
+/** Executes {@link Session} overlay changes on the main thread. */
+/* package */ final class TunerSessionOverlay implements Handler.Callback {
+
+    /** Displays the given {@link String} message object in the message view. */
+    public static final int MSG_UI_SHOW_MESSAGE = 1;
+    /** Hides the message view. Does not expect a message object. */
+    public static final int MSG_UI_HIDE_MESSAGE = 2;
+    /**
+     * Displays a message in the audio status view to signal audio is not supported. Does not expect
+     * a message object.
+     */
+    public static final int MSG_UI_SHOW_AUDIO_UNPLAYABLE = 3;
+    /** Hides the audio status view. Does not expect a message object. */
+    public static final int MSG_UI_HIDE_AUDIO_UNPLAYABLE = 4;
+    /** Feeds the given {@link CaptionEvent} message object to the {@link CaptionTrackRenderer}. */
+    public static final int MSG_UI_PROCESS_CAPTION_TRACK = 5;
+    /**
+     * Invokes {@link CaptionTrackRenderer#start(AtscCaptionTrack)} passing the given {@link
+     * AtscCaptionTrack} message object as argument.
+     */
+    public static final int MSG_UI_START_CAPTION_TRACK = 6;
+    /** Invokes {@link CaptionTrackRenderer#stop()}. Does not expect a message object. */
+    public static final int MSG_UI_STOP_CAPTION_TRACK = 7;
+    /** Invokes {@link CaptionTrackRenderer#reset()}. Does not expect a message object. */
+    public static final int MSG_UI_RESET_CAPTION_TRACK = 8;
+    /** Invokes {@link CaptionTrackRenderer#clear()}. Does not expect a message object. */
+    public static final int MSG_UI_CLEAR_CAPTION_RENDERER = 9;
+    /** Displays the given {@link CharSequence} message object in the status view. */
+    public static final int MSG_UI_SET_STATUS_TEXT = 10;
+    /** Displays a toast signalling that a re-scan is required. Does not expect a message object. */
+    public static final int MSG_UI_TOAST_RESCAN_NEEDED = 11;
+
+    private static final String USBTUNER_SHOW_DEBUG = "persist.tv.tuner.show_debug";
+
+    private final Context mContext;
+    private final Handler mHandler;
+    private final View mOverlayView;
+    private final TextView mMessageView;
+    private final TextView mStatusView;
+    private final TextView mAudioStatusView;
+    private final ViewGroup mMessageLayout;
+    private final CaptionTrackRenderer mCaptionTrackRenderer;
+
+    /**
+     * Creates and inflates a {@link Session} overlay from the given context.
+     *
+     * @param context The {@link Context} of the {@link Session}.
+     */
+    public TunerSessionOverlay(Context context) {
+        mContext = context;
+        mHandler = new Handler(this);
+        LayoutInflater inflater =
+                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        boolean showDebug = SystemPropertiesProxy.getBoolean(USBTUNER_SHOW_DEBUG, false);
+        mOverlayView = inflater.inflate(R.layout.ut_overlay_view, null);
+        mMessageLayout = mOverlayView.findViewById(R.id.message_layout);
+        mMessageLayout.setVisibility(View.INVISIBLE);
+        mMessageView = mOverlayView.findViewById(R.id.message);
+        mStatusView = mOverlayView.findViewById(R.id.tuner_status);
+        mStatusView.setVisibility(showDebug ? View.VISIBLE : View.INVISIBLE);
+        mAudioStatusView = mOverlayView.findViewById(R.id.audio_status);
+        mAudioStatusView.setVisibility(View.INVISIBLE);
+        CaptionLayout captionLayout = mOverlayView.findViewById(R.id.caption);
+        mCaptionTrackRenderer = new CaptionTrackRenderer(captionLayout);
+    }
+
+    /** Clears any pending messages in the message queue. */
+    public void release() {
+        mHandler.removeCallbacksAndMessages(null);
+    }
+
+    /** Returns a {@link View} representation of the overlay. */
+    public View getOverlayView() {
+        return mOverlayView;
+    }
+
+    /**
+     * Posts a message to be handled on the main thread. Only messages that do not expect a message
+     * object may be posted through this method.
+     *
+     * @param message One of the {@code MSG_UI_*} constants.
+     */
+    public void sendUiMessage(int message) {
+        mHandler.sendEmptyMessage(message);
+    }
+
+    /**
+     * Posts a message to be handled on the main thread.
+     *
+     * @param message One of the {@code MSG_UI_*} constants.
+     * @param object The object of the message. The required message object type depends on the
+     *     message being posted.
+     */
+    public void sendUiMessage(int message, Object object) {
+        mHandler.obtainMessage(message, object).sendToTarget();
+    }
+
+    @Override
+    public boolean handleMessage(Message msg) {
+        switch (msg.what) {
+            case MSG_UI_SHOW_MESSAGE:
+                mMessageView.setText((String) msg.obj);
+                mMessageLayout.setVisibility(View.VISIBLE);
+                return true;
+            case MSG_UI_HIDE_MESSAGE:
+                mMessageLayout.setVisibility(View.INVISIBLE);
+                return true;
+            case MSG_UI_SHOW_AUDIO_UNPLAYABLE:
+                // Showing message of enabling surround sound only when global surround sound
+                // setting is "never".
+                final int value = GlobalSettingsUtils.getEncodedSurroundOutputSettings(mContext);
+                if (value == GlobalSettingsUtils.ENCODED_SURROUND_OUTPUT_NEVER) {
+                    mAudioStatusView.setText(
+                            Html.fromHtml(
+                                    StatusTextUtils.getAudioWarningInHTML(
+                                            mContext.getString(
+                                                    R.string.ut_surround_sound_disabled))));
+                } else {
+                    mAudioStatusView.setText(
+                            Html.fromHtml(
+                                    StatusTextUtils.getAudioWarningInHTML(
+                                            mContext.getString(
+                                                    R.string.audio_passthrough_not_supported))));
+                }
+                mAudioStatusView.setVisibility(View.VISIBLE);
+                return true;
+            case MSG_UI_HIDE_AUDIO_UNPLAYABLE:
+                mAudioStatusView.setVisibility(View.INVISIBLE);
+                return true;
+            case MSG_UI_PROCESS_CAPTION_TRACK:
+                mCaptionTrackRenderer.processCaptionEvent((CaptionEvent) msg.obj);
+                return true;
+            case MSG_UI_START_CAPTION_TRACK:
+                mCaptionTrackRenderer.start((AtscCaptionTrack) msg.obj);
+                return true;
+            case MSG_UI_STOP_CAPTION_TRACK:
+                mCaptionTrackRenderer.stop();
+                return true;
+            case MSG_UI_RESET_CAPTION_TRACK:
+                mCaptionTrackRenderer.reset();
+                return true;
+            case MSG_UI_CLEAR_CAPTION_RENDERER:
+                mCaptionTrackRenderer.clear();
+                return true;
+            case MSG_UI_SET_STATUS_TEXT:
+                mStatusView.setText((CharSequence) msg.obj);
+                return true;
+            case MSG_UI_TOAST_RESCAN_NEEDED:
+                Toast.makeText(mContext, R.string.ut_rescan_needed, Toast.LENGTH_LONG).show();
+                return true;
+            default:
+                return false;
+        }
+    }
+}
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java
index 65750e0..d3f9409 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java
@@ -34,6 +34,8 @@
 import android.os.SystemClock;
 import android.support.annotation.AnyThread;
 import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
 import android.support.annotation.WorkerThread;
 import android.text.Html;
 import android.text.TextUtils;
@@ -45,10 +47,12 @@
 import com.android.tv.common.CommonPreferences.TrickplaySetting;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.TvContentRatingCache;
+import com.android.tv.common.compat.TvInputConstantCompat;
 import com.android.tv.common.customization.CustomizationManager;
 import com.android.tv.common.customization.CustomizationManager.TRICKPLAY_MODE;
+import com.android.tv.common.experiments.Experiments;
+import com.android.tv.common.feature.CommonFeatures;
 import com.android.tv.common.util.SystemPropertiesProxy;
-import com.android.tv.tuner.TunerPreferences;
 import com.android.tv.tuner.data.Cea708Data;
 import com.android.tv.tuner.data.PsipData.EitItem;
 import com.android.tv.tuner.data.PsipData.TvTracksInterface;
@@ -61,12 +65,19 @@
 import com.android.tv.tuner.exoplayer.buffer.BufferManager;
 import com.android.tv.tuner.exoplayer.buffer.BufferManager.StorageManager;
 import com.android.tv.tuner.exoplayer.buffer.DvrStorageManager;
+import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener;
 import com.android.tv.tuner.exoplayer.buffer.TrickplayStorageManager;
+import com.android.tv.tuner.prefs.TunerPreferences;
 import com.android.tv.tuner.source.TsDataSource;
 import com.android.tv.tuner.source.TsDataSourceManager;
+import com.android.tv.tuner.ts.EventDetector.EventListener;
+import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
+import com.android.tv.tuner.tvinput.debug.TunerDebug;
 import com.android.tv.tuner.util.StatusTextUtils;
 import com.google.android.exoplayer.ExoPlayer;
 import com.google.android.exoplayer.audio.AudioCapabilities;
+import com.google.common.collect.ImmutableList;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Iterator;
@@ -84,9 +95,10 @@
         implements PlaybackBufferListener,
                 MpegTsPlayer.VideoEventListener,
                 MpegTsPlayer.Listener,
-                EventDetector.EventListener,
+                EventListener,
                 ChannelDataManager.ProgramInfoListener,
                 Handler.Callback {
+
     private static final String TAG = "TunerSessionWorker";
     private static final boolean DEBUG = false;
     private static final boolean ENABLE_PROFILER = true;
@@ -103,14 +115,13 @@
     public static final int MSG_TIMESHIFT_RESUME = 5;
     public static final int MSG_TIMESHIFT_SEEK_TO = 6;
     public static final int MSG_TIMESHIFT_SET_PLAYBACKPARAMS = 7;
-    public static final int MSG_AUDIO_CAPABILITIES_CHANGED = 8;
     public static final int MSG_UNBLOCKED_RATING = 9;
     public static final int MSG_TUNER_PREFERENCES_CHANGED = 10;
 
     // Private messages
-    private static final int MSG_TUNE = 1000;
+    @VisibleForTesting protected static final int MSG_TUNE = 1000;
     private static final int MSG_RELEASE = 1001;
-    private static final int MSG_RETRY_PLAYBACK = 1002;
+    @VisibleForTesting protected static final int MSG_RETRY_PLAYBACK = 1002;
     private static final int MSG_START_PLAYBACK = 1003;
     private static final int MSG_UPDATE_PROGRAM = 1008;
     private static final int MSG_SCHEDULE_OF_PROGRAMS = 1009;
@@ -120,7 +131,7 @@
     private static final int MSG_PARENTAL_CONTROLS = 1015;
     private static final int MSG_RESCHEDULE_PROGRAMS = 1016;
     private static final int MSG_BUFFER_START_TIME_CHANGED = 1017;
-    private static final int MSG_CHECK_SIGNAL = 1018;
+    @VisibleForTesting protected static final int MSG_CHECK_SIGNAL = 1018;
     private static final int MSG_DISCOVER_CAPTION_SERVICE_NUMBER = 1019;
     private static final int MSG_RESET_PLAYBACK = 1020;
     private static final int MSG_BUFFER_STATE_CHANGED = 1021;
@@ -128,6 +139,7 @@
     private static final int MSG_STOP_TUNE = 1023;
     private static final int MSG_SET_SURFACE = 1024;
     private static final int MSG_NOTIFY_AUDIO_TRACK_UPDATED = 1025;
+    @VisibleForTesting protected static final int MSG_CHECK_SIGNAL_STRENGTH = 1026;
 
     private static final int TS_PACKET_SIZE = 188;
     private static final int CHECK_NO_SIGNAL_INITIAL_DELAY_MS = 4000;
@@ -137,6 +149,7 @@
     private static final int RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS = 4000;
     private static final int RESCHEDULE_PROGRAMS_INTERVAL_MS = 10000;
     private static final int RESCHEDULE_PROGRAMS_TOLERANCE_MS = 2000;
+    private static final int CHECK_SIGNAL_STRENGTH_INTERVAL_MS = 5000;
     // The following 3s is defined empirically. This should be larger than 2s considering video
     // key frame interval in the TS stream.
     private static final int PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS = 3000;
@@ -162,6 +175,8 @@
     private static final int TRICKPLAY_MONITOR_INTERVAL_MS = 250;
     private static final int RELEASE_WAIT_INTERVAL_MS = 50;
     private static final long TRICKPLAY_OFF_DURATION_MS = TimeUnit.DAYS.toMillis(14);
+    private static final long SEEK_MARGIN_MS = TimeUnit.SECONDS.toMillis(2);
+    public static final ImmutableList<TvContentRating> NO_CONTENT_RATINGS = ImmutableList.of();
 
     // Since release() is done asynchronously, synchronization between multiple TunerSessionWorker
     // creation/release is required.
@@ -202,10 +217,12 @@
     private boolean mChannelBlocked;
     private TvContentRating mUnblockedContentRating;
     private long mLastPositionMs;
+    private final AudioCapabilitiesReceiverV1Wrapper mAudioCapabilitiesReceiver;
     private AudioCapabilities mAudioCapabilities;
     private long mLastLimitInBytes;
     private final TvContentRatingCache mTvContentRatingCache = TvContentRatingCache.getInstance();
     private final TunerSession mSession;
+    private final TunerSessionOverlay mTunerSessionOverlay;
     private final boolean mHasSoftwareAudioDecoder;
     private int mPlayerState = ExoPlayer.STATE_IDLE;
     private long mPreparingStartTimeMs;
@@ -214,24 +231,62 @@
     private boolean mIsActiveSession;
     private boolean mReleaseRequested; // Guarded by mReleaseLock
     private final Object mReleaseLock = new Object();
+    private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
+
+    private int mSignalStrength;
+    private long mRecordedProgramStartTimeMs;
 
     public TunerSessionWorker(
-            Context context, ChannelDataManager channelDataManager, TunerSession tunerSession) {
+            Context context,
+            ChannelDataManager channelDataManager,
+            TunerSession tunerSession,
+            TunerSessionOverlay tunerSessionOverlay,
+            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
+            TsDataSourceManager.Factory tsDataSourceManagerFactory) {
+        this(
+                context,
+                channelDataManager,
+                tunerSession,
+                tunerSessionOverlay,
+                null,
+                concurrentDvrPlaybackFlags,
+                tsDataSourceManagerFactory);
+    }
+
+    @VisibleForTesting
+    protected TunerSessionWorker(
+            Context context,
+            ChannelDataManager channelDataManager,
+            TunerSession tunerSession,
+            TunerSessionOverlay tunerSessionOverlay,
+            @Nullable Handler handler,
+            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
+            TsDataSourceManager.Factory tsDataSourceManagerFactory) {
+        this.mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
         if (DEBUG) Log.d(TAG, "TunerSessionWorker created");
         mContext = context;
-
-        // HandlerThread should be set up before it is registered as a listener in the all other
-        // components.
-        HandlerThread handlerThread = new HandlerThread(TAG);
-        handlerThread.start();
-        mHandler = new Handler(handlerThread.getLooper(), this);
+        if (handler != null) {
+            mHandler = handler;
+        } else {
+            // HandlerThread should be set up before it is registered as a listener in the all other
+            // components.
+            HandlerThread handlerThread = new HandlerThread(TAG);
+            handlerThread.start();
+            mHandler = new Handler(handlerThread.getLooper(), this);
+        }
         mSession = tunerSession;
+        mTunerSessionOverlay = tunerSessionOverlay;
         mChannelDataManager = channelDataManager;
         mChannelDataManager.setListener(this);
         mChannelDataManager.checkDataVersion(mContext);
-        mSourceManager = TsDataSourceManager.createSourceManager(false);
+        mSourceManager = tsDataSourceManagerFactory.create(false);
         mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
         mTvTracks = new ArrayList<>();
+        mAudioCapabilitiesReceiver =
+                new AudioCapabilitiesReceiverV1Wrapper(
+                        context, mHandler, this::handleMessageAudioCapabilitiesChanged);
+        AudioCapabilities audioCapabilities = mAudioCapabilitiesReceiver.register();
+        mHandler.post(() -> handleMessageAudioCapabilitiesChanged(audioCapabilities));
         mAudioTrackMap = new SparseArray<>();
         mCaptionTrackMap = new SparseArray<>();
         CaptioningManager captioningManager =
@@ -401,6 +456,7 @@
             // TODO reimplement for google3
             // Here disconnect ffmpeg
         }
+        mAudioCapabilitiesReceiver.unregister();
         mChannelDataManager.setListener(null);
         mHandler.removeCallbacksAndMessages(null);
         mHandler.sendEmptyMessage(MSG_RELEASE);
@@ -509,18 +565,18 @@
             return;
         }
         Log.i(TAG, "AC3 audio cannot be played due to device limitation");
-        mSession.sendUiMessage(TunerSession.MSG_UI_SHOW_AUDIO_UNPLAYABLE);
+        mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_SHOW_AUDIO_UNPLAYABLE);
     }
 
     // MpegTsPlayer.VideoEventListener
     @Override
     public void onEmitCaptionEvent(Cea708Data.CaptionEvent event) {
-        mSession.sendUiMessage(TunerSession.MSG_UI_PROCESS_CAPTION_TRACK, event);
+        mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_PROCESS_CAPTION_TRACK, event);
     }
 
     @Override
     public void onClearCaptionEvent() {
-        mSession.sendUiMessage(TunerSession.MSG_UI_CLEAR_CAPTION_RENDERER);
+        mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_CLEAR_CAPTION_RENDERER);
     }
 
     @Override
@@ -541,7 +597,7 @@
 
     @Override
     public void onRescanNeeded() {
-        mSession.sendUiMessage(TunerSession.MSG_UI_TOAST_RESCAN_NEEDED);
+        mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_TOAST_RESCAN_NEEDED);
     }
 
     @Override
@@ -596,10 +652,12 @@
     private static class RecordedProgram {
         //        private final long mChannelId;
         private final String mDataUri;
+        private final long mStartTimeMillis;
 
         private static final String[] PROJECTION = {
             TvContract.Programs.COLUMN_CHANNEL_ID,
             TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI,
+            TvContract.RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS,
         };
 
         public RecordedProgram(Cursor cursor) {
@@ -607,11 +665,13 @@
             //            mChannelId = cursor.getLong(index++);
             index++;
             mDataUri = cursor.getString(index++);
+            mStartTimeMillis = cursor.getLong(index++);
         }
 
         public RecordedProgram(long channelId, String dataUri) {
             //            mChannelId = channelId;
             mDataUri = dataUri;
+            mStartTimeMillis = 0;
         }
 
         public static RecordedProgram onQuery(Cursor c) {
@@ -625,6 +685,10 @@
         public String getDataUri() {
             return mDataUri;
         }
+
+        public long getStartTime() {
+            return mStartTimeMillis;
+        }
     }
 
     private RecordedProgram getRecordedProgram(Uri recordedUri) {
@@ -650,6 +714,7 @@
     private String parseRecording(Uri uri) {
         RecordedProgram recording = getRecordedProgram(uri);
         if (recording != null) {
+            mRecordedProgramStartTimeMs = recording.getStartTime();
             return recording.getDataUri();
         }
         return null;
@@ -659,514 +724,630 @@
     public boolean handleMessage(Message msg) {
         switch (msg.what) {
             case MSG_TUNE:
-                {
-                    if (DEBUG) Log.d(TAG, "MSG_TUNE");
-
-                    // When sequential tuning messages arrived, it skips middle tuning messages in
-                    // order
-                    // to change to the last requested channel quickly.
-                    if (mHandler.hasMessages(MSG_TUNE)) {
-                        return true;
-                    }
-                    notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
-                    if (!mIsActiveSession) {
-                        // Wait until release is finished if there is a pending release.
-                        try {
-                            while (!sActiveSessionSemaphore.tryAcquire(
-                                    RELEASE_WAIT_INTERVAL_MS, TimeUnit.MILLISECONDS)) {
-                                synchronized (mReleaseLock) {
-                                    if (mReleaseRequested) {
-                                        return true;
-                                    }
-                                }
-                            }
-                        } catch (InterruptedException e) {
-                            Thread.currentThread().interrupt();
-                        }
-                        synchronized (mReleaseLock) {
-                            if (mReleaseRequested) {
-                                sActiveSessionSemaphore.release();
-                                return true;
-                            }
-                        }
-                        mIsActiveSession = true;
-                    }
-                    Uri channelUri = (Uri) msg.obj;
-                    String recording = null;
-                    long channelId = parseChannel(channelUri);
-                    TunerChannel channel =
-                            (channelId == -1) ? null : mChannelDataManager.getChannel(channelId);
-                    if (channelId == -1) {
-                        recording = parseRecording(channelUri);
-                    }
-                    if (channel == null && recording == null) {
-                        Log.w(TAG, "onTune() is failed. Can't find channel for " + channelUri);
-                        stopTune();
-                        notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
-                        return true;
-                    }
-                    clearCallbacksAndMessagesSafely();
-                    mChannelDataManager.removeAllCallbacksAndMessages();
-                    if (channel != null) {
-                        if (mTvInputManager.isParentalControlsEnabled() && channel.isLocked()) {
-                            Log.i(TAG, "onTune() is failed. Channel is blocked" + channel);
-                            mSession.notifyContentBlocked(TvContentRating.UNRATED);
-                            return true;
-                        }
-                        mChannelDataManager.requestProgramsData(channel);
-                    }
-                    prepareTune(channel, recording);
-                    // TODO: Need to refactor. notifyContentAllowed() should not be called if
-                    // parental
-                    // control is turned on.
-                    mSession.notifyContentAllowed();
-                    resetTvTracks();
-                    resetPlayback();
-                    mHandler.sendEmptyMessageDelayed(
-                            MSG_RESCHEDULE_PROGRAMS, RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS);
-                    return true;
-                }
+                return handleMessageTune((Uri) msg.obj);
             case MSG_STOP_TUNE:
-                {
-                    if (DEBUG) Log.d(TAG, "MSG_STOP_TUNE");
-                    mChannel = null;
-                    stopPlayback(true);
-                    stopCaptionTrack();
-                    resetTvTracks();
-                    notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
-                    return true;
-                }
+                return handleMessageStopTune();
             case MSG_RELEASE:
-                {
-                    if (DEBUG) Log.d(TAG, "MSG_RELEASE");
-                    mHandler.removeCallbacksAndMessages(null);
-                    stopPlayback(true);
-                    stopCaptionTrack();
-                    mSourceManager.release();
-                    mHandler.getLooper().quitSafely();
-                    if (mIsActiveSession) {
-                        sActiveSessionSemaphore.release();
-                    }
-                    return true;
-                }
+                return handleMessageRelease();
             case MSG_RETRY_PLAYBACK:
-                {
-                    if (System.identityHashCode(mPlayer) == (int) msg.obj) {
-                        Log.i(TAG, "Retrying the playback for channel: " + mChannel);
-                        mHandler.removeMessages(MSG_RETRY_PLAYBACK);
-                        // When there is a request of retrying playback, don't reuse TunerHal.
-                        mSourceManager.setKeepTuneStatus(false);
-                        mRetryCount++;
-                        if (DEBUG) {
-                            Log.d(TAG, "MSG_RETRY_PLAYBACK " + mRetryCount);
-                        }
-                        mChannelDataManager.removeAllCallbacksAndMessages();
-                        if (mRetryCount <= MAX_IMMEDIATE_RETRY_COUNT) {
-                            resetPlayback();
-                        } else {
-                            // When it reaches this point, it may be due to an error that occurred
-                            // in
-                            // the tuner device. Calling stopPlayback() resets the tuner device
-                            // to recover from the error.
-                            stopPlayback(false);
-                            stopCaptionTrack();
-
-                            notifyVideoUnavailable(
-                                    TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
-                            Log.i(TAG, "Notify weak signal since fail to retry playback");
-
-                            // After MAX_IMMEDIATE_RETRY_COUNT, give some delay of an empirically
-                            // chosen
-                            // value before recovering the playback.
-                            mHandler.sendEmptyMessageDelayed(
-                                    MSG_RESET_PLAYBACK, RECOVER_STOPPED_PLAYBACK_PERIOD_MS);
-                        }
-                    }
-                    return true;
-                }
+                return handleMessageRetryPlayback((int) msg.obj);
             case MSG_RESET_PLAYBACK:
-                {
-                    if (DEBUG) Log.d(TAG, "MSG_RESET_PLAYBACK");
-                    mChannelDataManager.removeAllCallbacksAndMessages();
-                    resetPlayback();
-                    return true;
-                }
+                return handleMessageResetPlayback();
             case MSG_START_PLAYBACK:
-                {
-                    if (DEBUG) Log.d(TAG, "MSG_START_PLAYBACK");
-                    if (mChannel != null || mRecordingId != null) {
-                        startPlayback((int) msg.obj);
-                    }
-                    return true;
-                }
+                return handleMessageStartPlayback((int) msg.obj);
             case MSG_UPDATE_PROGRAM:
-                {
-                    if (mChannel != null) {
-                        EitItem program = (EitItem) msg.obj;
-                        updateTvTracks(program, false);
-                        mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
-                    }
-                    return true;
-                }
+                return handleMessageUpdateProgram((EitItem) msg.obj);
             case MSG_SCHEDULE_OF_PROGRAMS:
-                {
-                    mHandler.removeMessages(MSG_UPDATE_PROGRAM);
-                    Pair<TunerChannel, List<EitItem>> pair =
-                            (Pair<TunerChannel, List<EitItem>>) msg.obj;
-                    TunerChannel channel = pair.first;
-                    if (mChannel == null) {
-                        return true;
-                    }
-                    if (mChannel != null && mChannel.compareTo(channel) != 0) {
-                        return true;
-                    }
-                    mPrograms = pair.second;
-                    EitItem currentProgram = getCurrentProgram();
-                    if (currentProgram == null) {
-                        mProgram = null;
-                    }
-                    long currentTimeMs = getCurrentPosition();
-                    if (mPrograms != null) {
-                        for (EitItem item : mPrograms) {
-                            if (currentProgram != null && currentProgram.compareTo(item) == 0) {
-                                if (DEBUG) {
-                                    Log.d(TAG, "Update current TvTracks " + item);
-                                }
-                                if (mProgram != null && mProgram.compareTo(item) == 0) {
-                                    continue;
-                                }
-                                mProgram = item;
-                                updateTvTracks(item, false);
-                            } else if (item.getStartTimeUtcMillis() > currentTimeMs) {
-                                if (DEBUG) {
-                                    Log.d(
-                                            TAG,
-                                            "Update next TvTracks "
-                                                    + item
-                                                    + " "
-                                                    + (item.getStartTimeUtcMillis()
-                                                            - currentTimeMs));
-                                }
-                                mHandler.sendMessageDelayed(
-                                        mHandler.obtainMessage(MSG_UPDATE_PROGRAM, item),
-                                        item.getStartTimeUtcMillis() - currentTimeMs);
-                            }
-                        }
-                    }
-                    mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
-                    return true;
-                }
+                // TODO: fix the unchecked cast waring.
+                Pair<TunerChannel, List<EitItem>> pair =
+                        (Pair<TunerChannel, List<EitItem>>) msg.obj;
+                return handleMessageScheduleOfPrograms(pair);
             case MSG_UPDATE_CHANNEL_INFO:
-                {
-                    TunerChannel channel = (TunerChannel) msg.obj;
-                    if (mChannel != null && mChannel.compareTo(channel) == 0) {
-                        updateChannelInfo(channel);
-                    }
-                    return true;
-                }
+                return handleMessageUpdateChannelInfo((TunerChannel) msg.obj);
             case MSG_PROGRAM_DATA_RESULT:
-                {
-                    TunerChannel channel = (TunerChannel) ((Pair) msg.obj).first;
-
-                    // If there already exists, skip it since real-time data is a top priority,
-                    if (mChannel != null
-                            && mChannel.compareTo(channel) == 0
-                            && mPrograms == null
-                            && mProgram == null) {
-                        sendMessage(MSG_SCHEDULE_OF_PROGRAMS, msg.obj);
-                    }
-                    return true;
-                }
+                return handleMessageProgramDataResult(msg);
             case MSG_TRICKPLAY_BY_SEEK:
-                {
-                    if (mPlayer == null) {
-                        return true;
-                    }
-                    doTrickplayBySeek(msg.arg1);
-                    return true;
-                }
+                return handleMessageTrickplayBySeek(msg.arg1);
             case MSG_SMOOTH_TRICKPLAY_MONITOR:
-                {
-                    if (mPlayer == null) {
-                        return true;
-                    }
-                    long systemCurrentTime = System.currentTimeMillis();
-                    long position = getCurrentPosition();
-                    if (mRecordingId == null) {
-                        // Checks if the position exceeds the upper bound when forwarding,
-                        // or exceed the lower bound when rewinding.
-                        // If the direction is not checked, there can be some issues.
-                        // (See b/29939781 for more details.)
-                        if ((position > systemCurrentTime && mPlaybackParams.getSpeed() > 0L)
-                                || (position < mBufferStartTimeMs
-                                        && mPlaybackParams.getSpeed() < 0L)) {
-                            doTimeShiftResume();
-                            return true;
-                        }
-                    } else {
-                        if (position > mRecordingDuration || position < 0) {
-                            doTimeShiftPause();
-                            return true;
-                        }
-                    }
-                    mHandler.sendEmptyMessageDelayed(
-                            MSG_SMOOTH_TRICKPLAY_MONITOR, TRICKPLAY_MONITOR_INTERVAL_MS);
-                    return true;
-                }
+                return handleMessageSmoothTrickplayMonitor();
             case MSG_RESCHEDULE_PROGRAMS:
-                {
-                    if (mHandler.hasMessages(MSG_SCHEDULE_OF_PROGRAMS)) {
-                        mHandler.sendEmptyMessage(MSG_RESCHEDULE_PROGRAMS);
-                    } else {
-                        doReschedulePrograms();
-                    }
-                    return true;
-                }
+                return handleMessageReschedulePrograms();
             case MSG_PARENTAL_CONTROLS:
-                {
-                    doParentalControls();
-                    mHandler.removeMessages(MSG_PARENTAL_CONTROLS);
-                    mHandler.sendEmptyMessageDelayed(
-                            MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS);
-                    return true;
-                }
+                return handleMessageParentalControl();
             case MSG_UNBLOCKED_RATING:
-                {
-                    mUnblockedContentRating = (TvContentRating) msg.obj;
-                    doParentalControls();
-                    mHandler.removeMessages(MSG_PARENTAL_CONTROLS);
-                    mHandler.sendEmptyMessageDelayed(
-                            MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS);
-                    return true;
-                }
+                return handleMessageUnblockedRating((TvContentRating) msg.obj);
             case MSG_DISCOVER_CAPTION_SERVICE_NUMBER:
-                {
-                    int serviceNumber = (int) msg.obj;
-                    doDiscoverCaptionServiceNumber(serviceNumber);
-                    return true;
-                }
+                return handleMessageDiscoverCaptionServiceNumber((int) msg.obj);
             case MSG_SELECT_TRACK:
-                {
-                    if (mPlayer == null) {
-                        Log.w(TAG, "mPlayer is null when doselectTrack is called");
-                        return false;
-                    }
-                    if (mChannel != null || mRecordingId != null) {
-                        doSelectTrack(msg.arg1, (String) msg.obj);
-                    }
-                    return true;
-                }
+                return handleMessageSelectTrack(msg.arg1, (String) msg.obj);
             case MSG_UPDATE_CAPTION_TRACK:
-                {
-                    if (mCaptionEnabled) {
-                        startCaptionTrack();
-                    } else {
-                        stopCaptionTrack();
-                    }
-                    return true;
-                }
+                return handleMessageUpdateCaptionTrack();
             case MSG_TIMESHIFT_PAUSE:
-                {
-                    if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_PAUSE");
-                    if (mPlayer == null) {
-                        return true;
-                    }
-                    setTrickplayEnabledIfNeeded();
-                    doTimeShiftPause();
-                    return true;
-                }
+                return handleMessageTimeshiftPause();
             case MSG_TIMESHIFT_RESUME:
-                {
-                    if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_RESUME");
-                    if (mPlayer == null) {
-                        return true;
-                    }
-                    setTrickplayEnabledIfNeeded();
-                    doTimeShiftResume();
-                    return true;
-                }
+                return handleMessageTimeshiftResume();
             case MSG_TIMESHIFT_SEEK_TO:
-                {
-                    long position = (long) msg.obj;
-                    if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_SEEK_TO (position=" + position + ")");
-                    if (mPlayer == null) {
-                        return true;
-                    }
-                    setTrickplayEnabledIfNeeded();
-                    doTimeShiftSeekTo(position);
-                    return true;
-                }
+                return handleMessageTimeshiftSeekTo((long) msg.obj);
             case MSG_TIMESHIFT_SET_PLAYBACKPARAMS:
-                {
-                    if (mPlayer == null) {
-                        return true;
-                    }
-                    setTrickplayEnabledIfNeeded();
-                    doTimeShiftSetPlaybackParams((PlaybackParams) msg.obj);
-                    return true;
-                }
-            case MSG_AUDIO_CAPABILITIES_CHANGED:
-                {
-                    AudioCapabilities capabilities = (AudioCapabilities) msg.obj;
-                    if (DEBUG) {
-                        Log.d(TAG, "MSG_AUDIO_CAPABILITIES_CHANGED " + capabilities);
-                    }
-                    if (capabilities == null) {
-                        return true;
-                    }
-                    if (!capabilities.equals(mAudioCapabilities)) {
-                        // HDMI supported encodings are changed. restart player.
-                        mAudioCapabilities = capabilities;
-                        resetPlayback();
-                    }
-                    return true;
-                }
+                return handleMessageTimeshiftSetPlaybackParams((PlaybackParams) msg.obj);
             case MSG_SET_STREAM_VOLUME:
-                {
-                    if (mPlayer != null && mPlayer.isPlaying()) {
-                        mPlayer.setVolume(mVolume);
-                    }
-                    return true;
-                }
+                return handleMessageSetStreamVolume();
             case MSG_TUNER_PREFERENCES_CHANGED:
-                {
-                    mHandler.removeMessages(MSG_TUNER_PREFERENCES_CHANGED);
-                    @TrickplaySetting
-                    int trickplaySetting = TunerPreferences.getTrickplaySetting(mContext);
-                    if (trickplaySetting != mTrickplaySetting) {
-                        boolean wasTrcikplayEnabled =
-                                mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED;
-                        boolean isTrickplayEnabled =
-                                trickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED;
-                        mTrickplaySetting = trickplaySetting;
-                        if (isTrickplayEnabled != wasTrcikplayEnabled) {
-                            sendMessage(MSG_RESET_PLAYBACK, System.identityHashCode(mPlayer));
+                return handleMessageTunerPreferencesChanged();
+            case MSG_BUFFER_START_TIME_CHANGED:
+                return handleMessageBufferStartTimeChanged((long) msg.obj);
+            case MSG_BUFFER_STATE_CHANGED:
+                return handleMessageBufferStateChanged((boolean) msg.obj);
+            case MSG_CHECK_SIGNAL:
+                return handleMessageCheckSignal();
+            case MSG_SET_SURFACE:
+                return handleMessageSetSurface();
+            case MSG_NOTIFY_AUDIO_TRACK_UPDATED:
+                return handleMessageAudioTrackUpdated();
+            case MSG_CHECK_SIGNAL_STRENGTH:
+                return handleMessageCheckSignalStrength();
+            default:
+                return unhandledMessage(msg);
+        }
+    }
+
+    private boolean handleMessageTune(Uri channelUri) {
+        if (DEBUG) {
+            Log.d(TAG, "MSG_TUNE");
+        }
+
+        // When sequential tuning messages arrived, it skips middle tuning messages in
+        // order
+        // to change to the last requested channel quickly.
+        if (mHandler.hasMessages(MSG_TUNE)) {
+            return true;
+        }
+        notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
+        if (!mIsActiveSession) {
+            // Wait until release is finished if there is a pending release.
+            try {
+                while (!sActiveSessionSemaphore.tryAcquire(
+                        RELEASE_WAIT_INTERVAL_MS, TimeUnit.MILLISECONDS)) {
+                    synchronized (mReleaseLock) {
+                        if (mReleaseRequested) {
+                            return true;
                         }
                     }
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+            synchronized (mReleaseLock) {
+                if (mReleaseRequested) {
+                    sActiveSessionSemaphore.release();
                     return true;
                 }
-            case MSG_BUFFER_START_TIME_CHANGED:
-                {
-                    if (mPlayer == null) {
-                        return true;
-                    }
-                    mBufferStartTimeMs = (long) msg.obj;
-                    if (!hasEnoughBackwardBuffer()
-                            && (!mPlayer.isPlaying() || mPlaybackParams.getSpeed() < 1.0f)) {
-                        mPlayer.setPlayWhenReady(true);
-                        mPlayer.setAudioTrackAndClosedCaption(true);
-                        mPlaybackParams.setSpeed(1.0f);
-                    }
-                    return true;
-                }
-            case MSG_BUFFER_STATE_CHANGED:
-                {
-                    boolean available = (boolean) msg.obj;
-                    mSession.notifyTimeShiftStatusChanged(
-                            available
-                                    ? TvInputManager.TIME_SHIFT_STATUS_AVAILABLE
-                                    : TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE);
-                    return true;
-                }
-            case MSG_CHECK_SIGNAL:
-                if (mChannel == null || mPlayer == null) {
-                    return true;
-                }
-                TsDataSource source = mPlayer.getDataSource();
-                long limitInBytes = source != null ? source.getBufferedPosition() : 0L;
-                if (TunerDebug.ENABLED) {
-                    TunerDebug.calculateDiff();
-                    mSession.sendUiMessage(
-                            TunerSession.MSG_UI_SET_STATUS_TEXT,
-                            Html.fromHtml(
-                                    StatusTextUtils.getStatusWarningInHTML(
-                                            (limitInBytes - mLastLimitInBytes) / TS_PACKET_SIZE,
-                                            TunerDebug.getVideoFrameDrop(),
-                                            TunerDebug.getBytesInQueue(),
-                                            TunerDebug.getAudioPositionUs(),
-                                            TunerDebug.getAudioPositionUsRate(),
-                                            TunerDebug.getAudioPtsUs(),
-                                            TunerDebug.getAudioPtsUsRate(),
-                                            TunerDebug.getVideoPtsUs(),
-                                            TunerDebug.getVideoPtsUsRate())));
-                }
-                mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE);
-                long currentTime = SystemClock.elapsedRealtime();
-                long bufferingTimeMs =
-                        mBufferingStartTimeMs != INVALID_TIME
-                                ? currentTime - mBufferingStartTimeMs
-                                : mBufferingStartTimeMs;
-                long preparingTimeMs =
-                        mPreparingStartTimeMs != INVALID_TIME
-                                ? currentTime - mPreparingStartTimeMs
-                                : mPreparingStartTimeMs;
-                boolean isBufferingTooLong =
-                        bufferingTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
-                boolean isPreparingTooLong =
-                        preparingTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
-                boolean isWeakSignal =
-                        source != null
-                                && mChannel.getType() != Channel.TunerType.TYPE_FILE
-                                && (isBufferingTooLong || isPreparingTooLong);
-                if (isWeakSignal && !mReportedWeakSignal) {
-                    if (!mHandler.hasMessages(MSG_RETRY_PLAYBACK)) {
-                        mHandler.sendMessageDelayed(
-                                mHandler.obtainMessage(
-                                        MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)),
-                                PLAYBACK_RETRY_DELAY_MS);
-                    }
-                    if (mPlayer != null) {
-                        mPlayer.setAudioTrackAndClosedCaption(false);
-                    }
-                    notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
-                    Log.i(
-                            TAG,
-                            "Notify weak signal due to signal check, "
-                                    + String.format(
-                                            "packetsPerSec:%d, bufferingTimeMs:%d, preparingTimeMs:%d, "
-                                                    + "videoFrameDrop:%d",
-                                            (limitInBytes - mLastLimitInBytes) / TS_PACKET_SIZE,
-                                            bufferingTimeMs,
-                                            preparingTimeMs,
-                                            TunerDebug.getVideoFrameDrop()));
-                } else if (!isWeakSignal && mReportedWeakSignal) {
-                    boolean isPlaybackStable =
-                            mReadyStartTimeMs != INVALID_TIME
-                                    && currentTime - mReadyStartTimeMs
-                                            > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
-                    if (!isPlaybackStable) {
-                        // Wait until playback becomes stable.
-                    } else if (mReportedDrawnToSurface) {
-                        mHandler.removeMessages(MSG_RETRY_PLAYBACK);
-                        notifyVideoAvailable();
-                        mPlayer.setAudioTrackAndClosedCaption(true);
-                    }
-                }
-                mLastLimitInBytes = limitInBytes;
-                mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_PERIOD_MS);
-                return true;
-            case MSG_SET_SURFACE:
-                {
-                    if (mPlayer != null) {
-                        mPlayer.setSurface(mSurface);
-                    } else {
-                        // TODO: Since surface is dynamically set, we can remove the dependency of
-                        // playback start on mSurface nullity.
-                        resetPlayback();
-                    }
-                    return true;
-                }
-            case MSG_NOTIFY_AUDIO_TRACK_UPDATED:
-                {
-                    notifyAudioTracksUpdated();
-                    return true;
-                }
-            default:
-                {
-                    Log.w(TAG, "Unhandled message code: " + msg.what);
-                    return false;
-                }
+            }
+            mIsActiveSession = true;
         }
+        String recording = null;
+        long channelId = parseChannel(channelUri);
+        TunerChannel channel = (channelId == -1) ? null : mChannelDataManager.getChannel(channelId);
+        if (channelId == -1) {
+            recording = parseRecording(channelUri);
+        }
+        if (channel == null && recording == null) {
+            Log.w(TAG, "onTune() is failed. Can't find channel for " + channelUri);
+            stopTune();
+            notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+            return true;
+        }
+        clearCallbacksAndMessagesSafely();
+        mChannelDataManager.removeAllCallbacksAndMessages();
+        if (channel != null) {
+            mChannelDataManager.requestProgramsData(channel);
+        }
+        prepareTune(channel, recording);
+        // TODO: Need to refactor. notifyContentAllowed() should not be called if
+        // parental
+        // control is turned on.
+        mSession.notifyContentAllowed();
+        resetTvTracks();
+        resetPlayback();
+        mHandler.sendEmptyMessageDelayed(
+                MSG_RESCHEDULE_PROGRAMS, RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS);
+        return true;
+    }
+
+    private boolean handleMessageStopTune() {
+        if (DEBUG) {
+            Log.d(TAG, "MSG_STOP_TUNE");
+        }
+        mChannel = null;
+        stopPlayback(true);
+        stopCaptionTrack();
+        resetTvTracks();
+        notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+        return true;
+    }
+
+    private boolean handleMessageRelease() {
+        if (DEBUG) {
+            Log.d(TAG, "MSG_RELEASE");
+        }
+        mHandler.removeCallbacksAndMessages(null);
+        stopPlayback(true);
+        stopCaptionTrack();
+        mSourceManager.release();
+        mHandler.getLooper().quitSafely();
+        if (mIsActiveSession) {
+            sActiveSessionSemaphore.release();
+        }
+        return true;
+    }
+
+    private boolean handleMessageRetryPlayback(int code) {
+        if (System.identityHashCode(mPlayer) == code) {
+            Log.i(TAG, "Retrying the playback for channel: " + mChannel);
+            mHandler.removeMessages(MSG_RETRY_PLAYBACK);
+            // When there is a request of retrying playback, don't reuse TunerHal.
+            mSourceManager.setKeepTuneStatus(false);
+            mRetryCount++;
+            if (DEBUG) {
+                Log.d(TAG, "MSG_RETRY_PLAYBACK " + mRetryCount);
+            }
+            mChannelDataManager.removeAllCallbacksAndMessages();
+            if (mRetryCount <= MAX_IMMEDIATE_RETRY_COUNT) {
+                resetPlayback();
+            } else {
+                // When it reaches this point, it may be due to an error that occurred
+                // in
+                // the tuner device. Calling stopPlayback() resets the tuner device
+                // to recover from the error.
+                stopPlayback(false);
+                stopCaptionTrack();
+
+                notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
+                Log.i(TAG, "Notify weak signal since fail to retry playback");
+
+                // After MAX_IMMEDIATE_RETRY_COUNT, give some delay of an empirically
+                // chosen
+                // value before recovering the playback.
+                mHandler.sendEmptyMessageDelayed(
+                        MSG_RESET_PLAYBACK, RECOVER_STOPPED_PLAYBACK_PERIOD_MS);
+            }
+        }
+        return true;
+    }
+
+    private boolean handleMessageResetPlayback() {
+        if (DEBUG) {
+            Log.d(TAG, "MSG_RESET_PLAYBACK");
+        }
+        mChannelDataManager.removeAllCallbacksAndMessages();
+        resetPlayback();
+        return true;
+    }
+
+    private boolean handleMessageStartPlayback(int playerHashCode) {
+        if (DEBUG) {
+            Log.d(TAG, "MSG_START_PLAYBACK");
+        }
+        if (mChannel != null || mRecordingId != null) {
+            startPlayback(playerHashCode);
+        }
+        return true;
+    }
+
+    private boolean handleMessageUpdateProgram(EitItem program) {
+        if (mChannel != null) {
+            updateTvTracks(program, false);
+            mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
+        }
+        return true;
+    }
+
+    private boolean handleMessageScheduleOfPrograms(Pair<TunerChannel, List<EitItem>> pair) {
+        mHandler.removeMessages(MSG_UPDATE_PROGRAM);
+        TunerChannel channel = pair.first;
+        if (mChannel == null) {
+            return true;
+        }
+        if (mChannel != null && mChannel.compareTo(channel) != 0) {
+            return true;
+        }
+        mPrograms = pair.second;
+        EitItem currentProgram = getCurrentProgram();
+        if (currentProgram == null) {
+            mProgram = null;
+        }
+        long currentTimeMs = getCurrentPosition();
+        if (mPrograms != null) {
+            for (EitItem item : mPrograms) {
+                if (currentProgram != null && currentProgram.compareTo(item) == 0) {
+                    if (DEBUG) {
+                        Log.d(TAG, "Update current TvTracks " + item);
+                    }
+                    if (mProgram != null && mProgram.compareTo(item) == 0) {
+                        continue;
+                    }
+                    mProgram = item;
+                    updateTvTracks(item, false);
+                } else if (item.getStartTimeUtcMillis() > currentTimeMs) {
+                    if (DEBUG) {
+                        Log.d(
+                                TAG,
+                                "Update next TvTracks "
+                                        + item
+                                        + " "
+                                        + (item.getStartTimeUtcMillis() - currentTimeMs));
+                    }
+                    mHandler.sendMessageDelayed(
+                            mHandler.obtainMessage(MSG_UPDATE_PROGRAM, item),
+                            item.getStartTimeUtcMillis() - currentTimeMs);
+                }
+            }
+        }
+        mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
+        return true;
+    }
+
+    private boolean handleMessageUpdateChannelInfo(TunerChannel tunerChannel) {
+        if (mChannel != null && mChannel.compareTo(tunerChannel) == 0) {
+            updateChannelInfo(tunerChannel);
+        }
+        return true;
+    }
+
+    private boolean handleMessageProgramDataResult(Message msg) {
+        TunerChannel channel = (TunerChannel) ((Pair) msg.obj).first;
+
+        // If there already exists, skip it since real-time data is a top priority,
+        if (mChannel != null
+                && mChannel.compareTo(channel) == 0
+                && mPrograms == null
+                && mProgram == null) {
+            sendMessage(MSG_SCHEDULE_OF_PROGRAMS, msg.obj);
+        }
+        return true;
+    }
+
+    private boolean handleMessageTrickplayBySeek(int seekPositionMs) {
+        if (mPlayer == null) {
+            return true;
+        }
+        if (mRecordingId != null) {
+            long systemBufferTime =
+                    System.currentTimeMillis() - SEEK_MARGIN_MS - mRecordedProgramStartTimeMs;
+            if (seekPositionMs > systemBufferTime) {
+                doTimeShiftResume();
+                return true;
+            }
+        }
+        doTrickplayBySeek(seekPositionMs);
+        return true;
+    }
+
+    private boolean handleMessageSmoothTrickplayMonitor() {
+        if (mPlayer == null) {
+            return true;
+        }
+        long systemCurrentTime = System.currentTimeMillis();
+        long position = getCurrentPosition();
+        if (mRecordingId == null) {
+            // Checks if the position exceeds the upper bound when forwarding,
+            // or exceed the lower bound when rewinding.
+            // If the direction is not checked, there can be some issues.
+            // (See b/29939781 for more details.)
+            if ((position > systemCurrentTime && mPlaybackParams.getSpeed() > 0L)
+                    || (position < mBufferStartTimeMs && mPlaybackParams.getSpeed() < 0L)) {
+                doTimeShiftResume();
+                return true;
+            }
+        } else {
+            if (position > mRecordingDuration || position < 0) {
+                doTimeShiftPause();
+                return true;
+            }
+            long systemBufferTime =
+                    systemCurrentTime - SEEK_MARGIN_MS - mRecordedProgramStartTimeMs;
+            if (position > systemBufferTime) {
+                doTimeShiftResume();
+                return true;
+            }
+        }
+        mHandler.sendEmptyMessageDelayed(
+                MSG_SMOOTH_TRICKPLAY_MONITOR, TRICKPLAY_MONITOR_INTERVAL_MS);
+        return true;
+    }
+
+    private boolean handleMessageReschedulePrograms() {
+        if (mHandler.hasMessages(MSG_SCHEDULE_OF_PROGRAMS)) {
+            mHandler.sendEmptyMessage(MSG_RESCHEDULE_PROGRAMS);
+        } else {
+            doReschedulePrograms();
+        }
+        return true;
+    }
+
+    private boolean handleMessageParentalControl() {
+        doParentalControls();
+        mHandler.removeMessages(MSG_PARENTAL_CONTROLS);
+        mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS);
+        return true;
+    }
+
+    private boolean handleMessageUnblockedRating(TvContentRating unblockedContentRating) {
+        mUnblockedContentRating = unblockedContentRating;
+        return handleMessageParentalControl();
+    }
+
+    private boolean handleMessageDiscoverCaptionServiceNumber(int serviceNumber) {
+        doDiscoverCaptionServiceNumber(serviceNumber);
+        return true;
+    }
+
+    private boolean handleMessageSelectTrack(int type, String trackId) {
+        if (mPlayer == null) {
+            Log.w(TAG, "mPlayer is null when doselectTrack is called");
+            return false;
+        }
+        if (mChannel != null || mRecordingId != null) {
+            doSelectTrack(type, trackId);
+        }
+        return true;
+    }
+
+    private boolean handleMessageUpdateCaptionTrack() {
+        if (mCaptionEnabled) {
+            startCaptionTrack();
+        } else {
+            stopCaptionTrack();
+        }
+        return true;
+    }
+
+    private boolean handleMessageTimeshiftPause() {
+        if (DEBUG) {
+            Log.d(TAG, "MSG_TIMESHIFT_PAUSE");
+        }
+        if (mPlayer == null) {
+            return true;
+        }
+        setTrickplayEnabledIfNeeded();
+        doTimeShiftPause();
+        return true;
+    }
+
+    private boolean handleMessageTimeshiftResume() {
+        if (DEBUG) {
+            Log.d(TAG, "MSG_TIMESHIFT_RESUME");
+        }
+        if (mPlayer == null) {
+            return true;
+        }
+        setTrickplayEnabledIfNeeded();
+        doTimeShiftResume();
+        return true;
+    }
+
+    private boolean handleMessageTimeshiftSeekTo(long timeMs) {
+        if (DEBUG) {
+            Log.d(TAG, "MSG_TIMESHIFT_SEEK_TO (position=" + timeMs + ")");
+        }
+        if (mPlayer == null) {
+            return true;
+        }
+        setTrickplayEnabledIfNeeded();
+        doTimeShiftSeekTo(timeMs);
+        return true;
+    }
+
+    private boolean handleMessageTimeshiftSetPlaybackParams(PlaybackParams playbackParams) {
+        if (mPlayer == null) {
+            return true;
+        }
+        setTrickplayEnabledIfNeeded();
+        doTimeShiftSetPlaybackParams(playbackParams);
+        return true;
+    }
+
+    private boolean handleMessageAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) {
+        if (DEBUG) {
+            Log.d(TAG, "MSG_AUDIO_CAPABILITIES_CHANGED " + audioCapabilities);
+        }
+        if (audioCapabilities == null) {
+            return true;
+        }
+        if (!audioCapabilities.equals(mAudioCapabilities)) {
+            // HDMI supported encodings are changed. restart player.
+            mAudioCapabilities = audioCapabilities;
+            resetPlayback();
+        }
+        return true;
+    }
+
+    private boolean handleMessageSetStreamVolume() {
+        if (mPlayer != null && mPlayer.isPlaying()) {
+            mPlayer.setVolume(mVolume);
+        }
+        return true;
+    }
+
+    private boolean handleMessageTunerPreferencesChanged() {
+        mHandler.removeMessages(MSG_TUNER_PREFERENCES_CHANGED);
+        @TrickplaySetting int trickplaySetting = TunerPreferences.getTrickplaySetting(mContext);
+        if (trickplaySetting != mTrickplaySetting) {
+            boolean wasTrcikplayEnabled =
+                    mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED;
+            boolean isTrickplayEnabled =
+                    trickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED;
+            mTrickplaySetting = trickplaySetting;
+            if (isTrickplayEnabled != wasTrcikplayEnabled) {
+                sendMessage(MSG_RESET_PLAYBACK, System.identityHashCode(mPlayer));
+            }
+        }
+        return true;
+    }
+
+    private boolean handleMessageBufferStartTimeChanged(long bufferStartTimeMs) {
+        if (mPlayer == null) {
+            return true;
+        }
+        mBufferStartTimeMs = bufferStartTimeMs;
+        if (!hasEnoughBackwardBuffer()
+                && (!mPlayer.isPlaying() || mPlaybackParams.getSpeed() < 1.0f)) {
+            mPlayer.setPlayWhenReady(true);
+            mPlayer.setAudioTrackAndClosedCaption(true);
+            mPlaybackParams.setSpeed(1.0f);
+        }
+        return true;
+    }
+
+    private boolean handleMessageBufferStateChanged(boolean available) {
+        mSession.notifyTimeShiftStatusChanged(
+                available
+                        ? TvInputManager.TIME_SHIFT_STATUS_AVAILABLE
+                        : TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE);
+        return true;
+    }
+
+    private boolean handleMessageCheckSignal() {
+        if (mChannel == null || mPlayer == null) {
+            return true;
+        }
+        TsDataSource source = mPlayer.getDataSource();
+        long limitInBytes = source != null ? source.getBufferedPosition() : 0L;
+        if (TunerDebug.ENABLED) {
+            TunerDebug.calculateDiff();
+            mTunerSessionOverlay.sendUiMessage(
+                    TunerSessionOverlay.MSG_UI_SET_STATUS_TEXT,
+                    Html.fromHtml(
+                            StatusTextUtils.getStatusWarningInHTML(
+                                    (limitInBytes - mLastLimitInBytes) / TS_PACKET_SIZE,
+                                    TunerDebug.getVideoFrameDrop(),
+                                    TunerDebug.getBytesInQueue(),
+                                    TunerDebug.getAudioPositionUs(),
+                                    TunerDebug.getAudioPositionUsRate(),
+                                    TunerDebug.getAudioPtsUs(),
+                                    TunerDebug.getAudioPtsUsRate(),
+                                    TunerDebug.getVideoPtsUs(),
+                                    TunerDebug.getVideoPtsUsRate())));
+        }
+        mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_HIDE_MESSAGE);
+        long currentTime = SystemClock.elapsedRealtime();
+        long bufferingTimeMs =
+                mBufferingStartTimeMs != INVALID_TIME
+                        ? currentTime - mBufferingStartTimeMs
+                        : mBufferingStartTimeMs;
+        long preparingTimeMs =
+                mPreparingStartTimeMs != INVALID_TIME
+                        ? currentTime - mPreparingStartTimeMs
+                        : mPreparingStartTimeMs;
+        boolean isBufferingTooLong = bufferingTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
+        boolean isPreparingTooLong = preparingTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
+        boolean isWeakSignal =
+                source != null
+                        && mChannel.getType() != Channel.TunerType.TYPE_FILE
+                        && (isBufferingTooLong || isPreparingTooLong);
+        if (isWeakSignal && !mReportedWeakSignal) {
+            if (!mHandler.hasMessages(MSG_RETRY_PLAYBACK)) {
+                mHandler.sendMessageDelayed(
+                        mHandler.obtainMessage(
+                                MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)),
+                        PLAYBACK_RETRY_DELAY_MS);
+            }
+            if (mPlayer != null) {
+                mPlayer.setAudioTrackAndClosedCaption(false);
+            }
+            notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
+            if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) {
+                mSession.notifySignalStrength(0);
+                mHandler.removeMessages(MSG_CHECK_SIGNAL_STRENGTH);
+            }
+            Log.i(
+                    TAG,
+                    "Notify weak signal due to signal check, "
+                            + String.format(
+                                    "packetsPerSec:%d, bufferingTimeMs:%d, preparingTimeMs:%d, "
+                                            + "videoFrameDrop:%d",
+                                    (limitInBytes - mLastLimitInBytes) / TS_PACKET_SIZE,
+                                    bufferingTimeMs,
+                                    preparingTimeMs,
+                                    TunerDebug.getVideoFrameDrop()));
+        } else if (!isWeakSignal && mReportedWeakSignal) {
+            boolean isPlaybackStable =
+                    mReadyStartTimeMs != INVALID_TIME
+                            && currentTime - mReadyStartTimeMs
+                                    > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
+            if (!isPlaybackStable) {
+                // Wait until playback becomes stable.
+            } else if (mReportedDrawnToSurface) {
+                mHandler.removeMessages(MSG_RETRY_PLAYBACK);
+                notifyVideoAvailable();
+                mPlayer.setAudioTrackAndClosedCaption(true);
+                if (mHandler.hasMessages(MSG_CHECK_SIGNAL_STRENGTH)) {
+                    mHandler.removeMessages(MSG_CHECK_SIGNAL_STRENGTH);
+                }
+                if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) {
+                    sendMessage(MSG_CHECK_SIGNAL_STRENGTH);
+                }
+            }
+        }
+        mLastLimitInBytes = limitInBytes;
+        mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_PERIOD_MS);
+        return true;
+    }
+
+    private boolean handleMessageSetSurface() {
+        if (mPlayer != null) {
+            mPlayer.setSurface(mSurface);
+        } else {
+            // TODO: Since surface is dynamically set, we can remove the dependency of
+            // playback start on mSurface nullity.
+            resetPlayback();
+        }
+        return true;
+    }
+
+    private boolean handleMessageAudioTrackUpdated() {
+        notifyAudioTracksUpdated();
+        return true;
+    }
+
+    private boolean handleMessageCheckSignalStrength() {
+        if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) {
+            int signal;
+            if (mPlayer != null) {
+                TsDataSource source = mPlayer.getDataSource();
+                if (source != null) {
+                    signal = source.getSignalStrength();
+                    return handleSignal(signal);
+                }
+            }
+        }
+        return false;
+    }
+
+    @VisibleForTesting
+    protected boolean handleSignal(int signal) {
+        if (signal == TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED
+                || signal == TvInputConstantCompat.SIGNAL_STRENGTH_ERROR) {
+            notifySignal(signal);
+            return true;
+        }
+        if (signal != mSignalStrength && signal >= 0) {
+            notifySignal(signal);
+        }
+        mHandler.sendEmptyMessageDelayed(
+                MSG_CHECK_SIGNAL_STRENGTH, CHECK_SIGNAL_STRENGTH_INTERVAL_MS);
+        return true;
+    }
+
+    @VisibleForTesting
+    protected void notifySignal(int signal) {
+        mSession.notifySignalStrength(signal);
+        mSignalStrength = signal;
+    }
+
+    private boolean unhandledMessage(Message msg) {
+        Log.w(TAG, "Unhandled message code: " + msg.what);
+        return false;
     }
 
     // Private methods
@@ -1212,10 +1393,12 @@
         }
     }
 
-    private MpegTsPlayer createPlayer(AudioCapabilities capabilities) {
+    @VisibleForTesting
+    protected MpegTsPlayer createPlayer(AudioCapabilities capabilities) {
         if (capabilities == null) {
             Log.w(TAG, "No Audio Capabilities");
         }
+        mSourceManager.setKeepTuneStatus(true);
         long now = System.currentTimeMillis();
         if (mTrickplayModeCustomization == CustomizationManager.TRICKPLAY_MODE_ENABLED
                 && mTrickplaySetting == TunerPreferences.TRICKPLAY_SETTING_NOT_SET) {
@@ -1249,19 +1432,27 @@
         }
         MpegTsPlayer player =
                 new MpegTsPlayer(
-                        new MpegTsRendererBuilder(mContext, bufferManager, this),
+                        new MpegTsRendererBuilder(
+                                mContext, bufferManager, this, mConcurrentDvrPlaybackFlags),
                         mHandler,
                         mSourceManager,
                         capabilities,
                         this);
         Log.i(TAG, "Passthrough AC3 renderer");
         if (DEBUG) Log.d(TAG, "ExoPlayer created");
+        player.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER);
+        player.setVideoEventListener(this);
+        player.setCaptionServiceNumber(
+                mCaptionTrack != null
+                        ? mCaptionTrack.serviceNumber
+                        : Cea708Data.EMPTY_SERVICE_NUMBER);
         return player;
     }
 
     private void startCaptionTrack() {
         if (mCaptionEnabled && mCaptionTrack != null) {
-            mSession.sendUiMessage(TunerSession.MSG_UI_START_CAPTION_TRACK, mCaptionTrack);
+            mTunerSessionOverlay.sendUiMessage(
+                    TunerSessionOverlay.MSG_UI_START_CAPTION_TRACK, mCaptionTrack);
             if (mPlayer != null) {
                 mPlayer.setCaptionServiceNumber(mCaptionTrack.serviceNumber);
             }
@@ -1272,14 +1463,14 @@
         if (mPlayer != null) {
             mPlayer.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER);
         }
-        mSession.sendUiMessage(TunerSession.MSG_UI_STOP_CAPTION_TRACK);
+        mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_STOP_CAPTION_TRACK);
     }
 
     private void resetTvTracks() {
         mTvTracks.clear();
         mAudioTrackMap.clear();
         mCaptionTrackMap.clear();
-        mSession.sendUiMessage(TunerSession.MSG_UI_RESET_CAPTION_TRACK);
+        mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_RESET_CAPTION_TRACK);
         mSession.notifyTracksChanged(mTvTracks);
     }
 
@@ -1478,7 +1669,7 @@
             mBufferingStartTimeMs = INVALID_TIME;
             mReadyStartTimeMs = INVALID_TIME;
             mLastLimitInBytes = 0L;
-            mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_AUDIO_UNPLAYABLE);
+            mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_HIDE_AUDIO_UNPLAYABLE);
             mSession.notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE);
         }
     }
@@ -1518,24 +1709,14 @@
                 // Doesn't show buffering during weak signal.
                 notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING);
             }
-            mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE);
+            mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_HIDE_MESSAGE);
             mPlayerStarted = true;
         }
     }
 
-    private void preparePlayback() {
-        SoftPreconditions.checkState(mPlayer == null);
-        if (mChannel == null && mRecordingId == null) {
-            return;
-        }
-        mSourceManager.setKeepTuneStatus(true);
+    @VisibleForTesting
+    protected void preparePlayback() {
         MpegTsPlayer player = createPlayer(mAudioCapabilities);
-        player.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER);
-        player.setVideoEventListener(this);
-        player.setCaptionServiceNumber(
-                mCaptionTrack != null
-                        ? mCaptionTrack.serviceNumber
-                        : Cea708Data.EMPTY_SERVICE_NUMBER);
         if (!player.prepare(mContext, mChannel, mHasSoftwareAudioDecoder, this)) {
             mSourceManager.setKeepTuneStatus(false);
             player.release();
@@ -1554,6 +1735,12 @@
             mPlayerStarted = false;
             mHandler.removeMessages(MSG_CHECK_SIGNAL);
             mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_INITIAL_DELAY_MS);
+            if (mHandler.hasMessages(MSG_CHECK_SIGNAL_STRENGTH)) {
+                mHandler.removeMessages(MSG_CHECK_SIGNAL_STRENGTH);
+            }
+            if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) {
+                mHandler.sendEmptyMessage(MSG_CHECK_SIGNAL_STRENGTH);
+            }
         }
     }
 
@@ -1568,9 +1755,10 @@
             timestamp = SystemClock.elapsedRealtime();
             Log.i(TAG, "[Profiler] stopPlayback() takes " + (timestamp - oldTimestamp) + " ms");
         }
-        if (mChannelBlocked || mSurface == null) {
+        if (mChannelBlocked || mSurface == null || (mChannel == null && mRecordingId == null)) {
             return;
         }
+        SoftPreconditions.checkState(mPlayer == null);
         preparePlayback();
     }
 
@@ -1591,6 +1779,10 @@
         }
         mLastPositionMs = 0;
         mCaptionTrack = null;
+        mSignalStrength = TvInputConstantCompat.SIGNAL_STRENGTH_UNKNOWN;
+        if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) {
+            mSession.notifySignalStrength(mSignalStrength);
+        }
         mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
     }
 
@@ -1793,10 +1985,14 @@
         if (currentProgram == null) {
             return null;
         }
-        TvContentRating[] ratings =
+        ImmutableList<TvContentRating> ratings =
                 mTvContentRatingCache.getRatings(currentProgram.getContentRating());
-        if (ratings == null || ratings.length == 0) {
-            ratings = new TvContentRating[] {TvContentRating.UNRATED};
+        if ((ratings == null || ratings.isEmpty())) {
+            if (Experiments.ENABLE_UNRATED_CONTENT_SETTINGS.get()) {
+                ratings = ImmutableList.of(TvContentRating.UNRATED);
+            } else {
+                ratings = NO_CONTENT_RATINGS;
+            }
         }
         for (TvContentRating rating : ratings) {
             if (!Objects.equals(mUnblockedContentRating, rating)
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorkerExoV2.java b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorkerExoV2.java
new file mode 100644
index 0000000..82afff1
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorkerExoV2.java
@@ -0,0 +1,2073 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.tvinput;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.media.MediaFormat;
+import android.media.PlaybackParams;
+import android.media.tv.TvContentRating;
+import android.media.tv.TvContract;
+import android.media.tv.TvInputManager;
+import android.media.tv.TvTrackInfo;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.SystemClock;
+import android.support.annotation.AnyThread;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
+import android.text.Html;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.view.Surface;
+import android.view.accessibility.CaptioningManager;
+import com.android.tv.common.CommonPreferences.TrickplaySetting;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.TvContentRatingCache;
+import com.android.tv.common.compat.TvInputConstantCompat;
+import com.android.tv.common.customization.CustomizationManager;
+import com.android.tv.common.customization.CustomizationManager.TRICKPLAY_MODE;
+import com.android.tv.common.experiments.Experiments;
+import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.util.SystemPropertiesProxy;
+import com.android.tv.tuner.data.Cea708Data;
+import com.android.tv.tuner.data.PsipData.EitItem;
+import com.android.tv.tuner.data.PsipData.TvTracksInterface;
+import com.android.tv.tuner.data.TunerChannel;
+import com.android.tv.tuner.data.nano.Channel;
+import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.exoplayer.MpegTsPlayer;
+import com.android.tv.tuner.exoplayer.MpegTsRendererBuilder;
+import com.android.tv.tuner.exoplayer.buffer.BufferManager;
+import com.android.tv.tuner.exoplayer.buffer.BufferManager.StorageManager;
+import com.android.tv.tuner.exoplayer.buffer.DvrStorageManager;
+import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener;
+import com.android.tv.tuner.exoplayer.buffer.TrickplayStorageManager;
+import com.android.tv.tuner.prefs.TunerPreferences;
+import com.android.tv.tuner.source.TsDataSource;
+import com.android.tv.tuner.source.TsDataSourceManager;
+import com.android.tv.tuner.ts.EventDetector.EventListener;
+import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
+import com.android.tv.tuner.tvinput.debug.TunerDebug;
+import com.android.tv.tuner.util.StatusTextUtils;
+import com.google.android.exoplayer.ExoPlayer;
+import com.google.android.exoplayer.audio.AudioCapabilities;
+import com.google.common.collect.ImmutableList;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+/** Handles playback related operations on a worker thread. */
+@WorkerThread
+public class TunerSessionWorkerExoV2
+        implements PlaybackBufferListener,
+                MpegTsPlayer.VideoEventListener,
+                MpegTsPlayer.Listener,
+                EventListener,
+                ChannelDataManager.ProgramInfoListener,
+                Handler.Callback {
+
+    private static final String TAG = "TunerSessionWorkerExoV2";
+    private static final boolean DEBUG = false;
+    private static final boolean ENABLE_PROFILER = true;
+    private static final String PLAY_FROM_CHANNEL = "channel";
+    private static final String MAX_BUFFER_SIZE_KEY = "tv.tuner.buffersize_mbytes";
+    private static final int MAX_BUFFER_SIZE_DEF = 2 * 1024; // 2GB
+    private static final int MIN_BUFFER_SIZE_DEF = 256; // 256MB
+
+    // Public messages
+    public static final int MSG_SELECT_TRACK = 1;
+    public static final int MSG_UPDATE_CAPTION_TRACK = 2;
+    public static final int MSG_SET_STREAM_VOLUME = 3;
+    public static final int MSG_TIMESHIFT_PAUSE = 4;
+    public static final int MSG_TIMESHIFT_RESUME = 5;
+    public static final int MSG_TIMESHIFT_SEEK_TO = 6;
+    public static final int MSG_TIMESHIFT_SET_PLAYBACKPARAMS = 7;
+    public static final int MSG_UNBLOCKED_RATING = 9;
+    public static final int MSG_TUNER_PREFERENCES_CHANGED = 10;
+
+    // Private messages
+    @VisibleForTesting protected static final int MSG_TUNE = 1000;
+    private static final int MSG_RELEASE = 1001;
+    @VisibleForTesting protected static final int MSG_RETRY_PLAYBACK = 1002;
+    private static final int MSG_START_PLAYBACK = 1003;
+    private static final int MSG_UPDATE_PROGRAM = 1008;
+    private static final int MSG_SCHEDULE_OF_PROGRAMS = 1009;
+    private static final int MSG_UPDATE_CHANNEL_INFO = 1010;
+    private static final int MSG_TRICKPLAY_BY_SEEK = 1011;
+    private static final int MSG_SMOOTH_TRICKPLAY_MONITOR = 1012;
+    private static final int MSG_PARENTAL_CONTROLS = 1015;
+    private static final int MSG_RESCHEDULE_PROGRAMS = 1016;
+    private static final int MSG_BUFFER_START_TIME_CHANGED = 1017;
+    @VisibleForTesting protected static final int MSG_CHECK_SIGNAL = 1018;
+    private static final int MSG_DISCOVER_CAPTION_SERVICE_NUMBER = 1019;
+    private static final int MSG_RESET_PLAYBACK = 1020;
+    private static final int MSG_BUFFER_STATE_CHANGED = 1021;
+    private static final int MSG_PROGRAM_DATA_RESULT = 1022;
+    private static final int MSG_STOP_TUNE = 1023;
+    private static final int MSG_SET_SURFACE = 1024;
+    private static final int MSG_NOTIFY_AUDIO_TRACK_UPDATED = 1025;
+    @VisibleForTesting protected static final int MSG_CHECK_SIGNAL_STRENGTH = 1026;
+
+    private static final int TS_PACKET_SIZE = 188;
+    private static final int CHECK_NO_SIGNAL_INITIAL_DELAY_MS = 4000;
+    private static final int CHECK_NO_SIGNAL_PERIOD_MS = 500;
+    private static final int RECOVER_STOPPED_PLAYBACK_PERIOD_MS = 2500;
+    private static final int PARENTAL_CONTROLS_INTERVAL_MS = 5000;
+    private static final int RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS = 4000;
+    private static final int RESCHEDULE_PROGRAMS_INTERVAL_MS = 10000;
+    private static final int RESCHEDULE_PROGRAMS_TOLERANCE_MS = 2000;
+    private static final int CHECK_SIGNAL_STRENGTH_INTERVAL_MS = 5000;
+    // The following 3s is defined empirically. This should be larger than 2s considering video
+    // key frame interval in the TS stream.
+    private static final int PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS = 3000;
+    private static final int PLAYBACK_RETRY_DELAY_MS = 5000;
+    private static final int MAX_IMMEDIATE_RETRY_COUNT = 5;
+    private static final long INVALID_TIME = -1;
+
+    // Some examples of the track ids of the audio tracks, "a0", "a1", "a2".
+    // The number after prefix is being used for indicating a index of the given audio track.
+    private static final String AUDIO_TRACK_PREFIX = "a";
+
+    // Some examples of the tracks id of the caption tracks, "s1", "s2", "s3".
+    // The number after prefix is being used for indicating a index of a caption service number
+    // of the given caption track.
+    private static final String SUBTITLE_TRACK_PREFIX = "s";
+    private static final int TRACK_PREFIX_SIZE = 1;
+    private static final String VIDEO_TRACK_ID = "v";
+    private static final long BUFFER_UNDERFLOW_BUFFER_MS = 5000;
+
+    // Actual interval would be divided by the speed.
+    private static final int EXPECTED_KEY_FRAME_INTERVAL_MS = 500;
+    private static final int MIN_TRICKPLAY_SEEK_INTERVAL_MS = 20;
+    private static final int TRICKPLAY_MONITOR_INTERVAL_MS = 250;
+    private static final int RELEASE_WAIT_INTERVAL_MS = 50;
+    private static final long TRICKPLAY_OFF_DURATION_MS = TimeUnit.DAYS.toMillis(14);
+    private static final long SEEK_MARGIN_MS = TimeUnit.SECONDS.toMillis(2);
+    public static final ImmutableList<TvContentRating> NO_CONTENT_RATINGS = ImmutableList.of();
+
+    /**
+     * Guarantees that at most one active worker exists at any give time. Synchronization between
+     * multiple TunerSessionWorkerExoV2 is necessary when concurrent release and creation takes
+     * place.
+     */
+    private static Semaphore sActiveSessionSemaphore = new Semaphore(1);
+
+    private final Context mContext;
+    private final ChannelDataManager mChannelDataManager;
+    private final TsDataSourceManager mSourceManager;
+    private final int mMaxTrickplayBufferSizeMb;
+    private final File mTrickplayBufferDir;
+    private final @TRICKPLAY_MODE int mTrickplayModeCustomization;
+    private volatile Surface mSurface;
+    private volatile float mVolume = 1.0f;
+    private volatile boolean mCaptionEnabled;
+    private volatile MpegTsPlayer mPlayer;
+    private volatile TunerChannel mChannel;
+    private volatile Long mRecordingDuration;
+    private volatile long mRecordStartTimeMs;
+    private volatile long mBufferStartTimeMs;
+    private volatile boolean mTrickplayDisabledByStorageIssue;
+    private @TrickplaySetting int mTrickplaySetting;
+    private long mTrickplayExpiredMs;
+    private String mRecordingId;
+    private final Handler mHandler;
+    private int mRetryCount;
+    private final ArrayList<TvTrackInfo> mTvTracks;
+    private final SparseArray<AtscAudioTrack> mAudioTrackMap;
+    private final SparseArray<AtscCaptionTrack> mCaptionTrackMap;
+    private AtscCaptionTrack mCaptionTrack;
+    private PlaybackParams mPlaybackParams = new PlaybackParams();
+    private boolean mPlayerStarted = false;
+    private boolean mReportedDrawnToSurface = false;
+    private boolean mReportedWeakSignal = false;
+    private EitItem mProgram;
+    private List<EitItem> mPrograms;
+    private final TvInputManager mTvInputManager;
+    private boolean mChannelBlocked;
+    private TvContentRating mUnblockedContentRating;
+    private long mLastPositionMs;
+    private final AudioCapabilitiesReceiverV1Wrapper mAudioCapabilitiesReceiver;
+    private AudioCapabilities mAudioCapabilities;
+    private long mLastLimitInBytes;
+    private final TvContentRatingCache mTvContentRatingCache = TvContentRatingCache.getInstance();
+    private final TunerSessionExoV2 mSession;
+    private final TunerSessionOverlay mTunerSessionOverlay;
+    private final boolean mHasSoftwareAudioDecoder;
+    private int mPlayerState = ExoPlayer.STATE_IDLE;
+    private long mPreparingStartTimeMs;
+    private long mBufferingStartTimeMs;
+    private long mReadyStartTimeMs;
+    private boolean mIsActiveSession;
+    private boolean mReleaseRequested; // Guarded by mReleaseLock
+    private final Object mReleaseLock = new Object();
+    private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
+
+    private int mSignalStrength;
+    private long mRecordedProgramStartTimeMs;
+
+    public TunerSessionWorkerExoV2(
+            Context context,
+            ChannelDataManager channelDataManager,
+            TunerSessionExoV2 tunerSession,
+            TunerSessionOverlay tunerSessionOverlay,
+            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
+            TsDataSourceManager.Factory tsDataSourceManagerFactory) {
+        this(
+                context,
+                channelDataManager,
+                tunerSession,
+                tunerSessionOverlay,
+                null,
+                concurrentDvrPlaybackFlags,
+                tsDataSourceManagerFactory);
+    }
+
+    @VisibleForTesting
+    protected TunerSessionWorkerExoV2(
+            Context context,
+            ChannelDataManager channelDataManager,
+            TunerSessionExoV2 tunerSession,
+            TunerSessionOverlay tunerSessionOverlay,
+            @Nullable Handler handler,
+            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
+            TsDataSourceManager.Factory tsDataSourceManagerFactory) {
+        mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
+        if (DEBUG) {
+            Log.d(TAG, "TunerSessionWorkerExoV2 created");
+        }
+        mContext = context;
+        if (handler != null) {
+            mHandler = handler;
+        } else {
+            // HandlerThread should be set up before it is registered as a listener in the all other
+            // components.
+            HandlerThread handlerThread = new HandlerThread(TAG);
+            handlerThread.start();
+            mHandler = new Handler(handlerThread.getLooper(), this);
+        }
+        mSession = tunerSession;
+        mTunerSessionOverlay = tunerSessionOverlay;
+        mChannelDataManager = channelDataManager;
+        mChannelDataManager.setListener(this);
+        mChannelDataManager.checkDataVersion(mContext);
+        mSourceManager = tsDataSourceManagerFactory.create(false);
+        mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
+        mTvTracks = new ArrayList<>();
+        mAudioCapabilitiesReceiver =
+                new AudioCapabilitiesReceiverV1Wrapper(
+                        context, mHandler, this::handleMessageAudioCapabilitiesChanged);
+        AudioCapabilities audioCapabilities = mAudioCapabilitiesReceiver.register();
+        mHandler.post(() -> handleMessageAudioCapabilitiesChanged(audioCapabilities));
+        mAudioTrackMap = new SparseArray<>();
+        mCaptionTrackMap = new SparseArray<>();
+        CaptioningManager captioningManager =
+                (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
+        mCaptionEnabled = captioningManager.isEnabled();
+        mPlaybackParams.setSpeed(1.0f);
+        mMaxTrickplayBufferSizeMb =
+                SystemPropertiesProxy.getInt(MAX_BUFFER_SIZE_KEY, MAX_BUFFER_SIZE_DEF);
+        mTrickplayModeCustomization = CustomizationManager.getTrickplayMode(context);
+        if (mTrickplayModeCustomization
+                == CustomizationManager.TRICKPLAY_MODE_USE_EXTERNAL_STORAGE) {
+            boolean useExternalStorage =
+                    Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
+                            && Environment.isExternalStorageRemovable();
+            mTrickplayBufferDir = useExternalStorage ? context.getExternalCacheDir() : null;
+        } else if (mTrickplayModeCustomization == CustomizationManager.TRICKPLAY_MODE_ENABLED) {
+            mTrickplayBufferDir = context.getCacheDir();
+        } else {
+            mTrickplayBufferDir = null;
+        }
+        mTrickplayDisabledByStorageIssue = mTrickplayBufferDir == null;
+        mTrickplaySetting = TunerPreferences.getTrickplaySetting(context);
+        if (mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_NOT_SET
+                && mTrickplayModeCustomization
+                        == CustomizationManager.TRICKPLAY_MODE_USE_EXTERNAL_STORAGE) {
+            // Consider the case of Customization package updates the value of trickplay mode
+            // to TRICKPLAY_MODE_USE_EXTERNAL_STORAGE after install.
+            mTrickplaySetting = TunerPreferences.TRICKPLAY_SETTING_NOT_SET;
+            TunerPreferences.setTrickplaySetting(context, mTrickplaySetting);
+            TunerPreferences.setTrickplayExpiredMs(context, 0);
+        }
+        mTrickplayExpiredMs = TunerPreferences.getTrickplayExpiredMs(context);
+        mPreparingStartTimeMs = INVALID_TIME;
+        mBufferingStartTimeMs = INVALID_TIME;
+        mReadyStartTimeMs = INVALID_TIME;
+        // Only one TunerSessionWorker can be connected to FfmpegDecoderClient at any given time.
+        // connect() will return false, if there is a connected TunerSessionWorker already.
+        mHasSoftwareAudioDecoder = false; // TODO reimplement ffmpeg for google3
+        // TODO connect the ffmpeg client and report if available.
+    }
+
+    // Public methods
+    @MainThread
+    public void tune(Uri channelUri) {
+        mHandler.removeCallbacksAndMessages(null);
+        mSourceManager.setHasPendingTune();
+        sendMessage(MSG_TUNE, channelUri);
+    }
+
+    @MainThread
+    public void stopTune() {
+        mHandler.removeCallbacksAndMessages(null);
+        sendMessage(MSG_STOP_TUNE);
+    }
+
+    /** Sets {@link Surface}. */
+    @MainThread
+    public void setSurface(Surface surface) {
+        if (surface != null && !surface.isValid()) {
+            Log.w(TAG, "Ignoring invalid surface.");
+            return;
+        }
+        // mSurface is kept even when tune is called right after. But, messages can be deleted by
+        // tune or updateChannelBlockStatus. So mSurface should be stored here, not through message.
+        mSurface = surface;
+        mHandler.sendEmptyMessage(MSG_SET_SURFACE);
+    }
+
+    /** Sets volume. */
+    @MainThread
+    public void setStreamVolume(float volume) {
+        // mVolume is kept even when tune is called right after. But, messages can be deleted by
+        // tune or updateChannelBlockStatus. So mVolume is stored here and mPlayer.setVolume will be
+        // called in MSG_SET_STREAM_VOLUME.
+        mVolume = volume;
+        mHandler.sendEmptyMessage(MSG_SET_STREAM_VOLUME);
+    }
+
+    /** Sets if caption is enabled or disabled. */
+    @MainThread
+    public void setCaptionEnabled(boolean captionEnabled) {
+        // mCaptionEnabled is kept even when tune is called right after. But, messages can be
+        // deleted by tune or updateChannelBlockStatus. So mCaptionEnabled is stored here and
+        // start/stopCaptionTrack will be called in MSG_UPDATE_CAPTION_STATUS.
+        mCaptionEnabled = captionEnabled;
+        mHandler.sendEmptyMessage(MSG_UPDATE_CAPTION_TRACK);
+    }
+
+    public TunerChannel getCurrentChannel() {
+        return mChannel;
+    }
+
+    @MainThread
+    public long getStartPosition() {
+        return mBufferStartTimeMs;
+    }
+
+    private String getRecordingPath() {
+        return Uri.parse(mRecordingId).getPath();
+    }
+
+    private Long getDurationForRecording(String recordingId) {
+        DvrStorageManager storageManager =
+                new DvrStorageManager(new File(getRecordingPath()), false);
+        List<BufferManager.TrackFormat> trackFormatList = storageManager.readTrackInfoFiles(false);
+        if (trackFormatList.isEmpty()) {
+            trackFormatList = storageManager.readTrackInfoFiles(true);
+        }
+        if (!trackFormatList.isEmpty()) {
+            BufferManager.TrackFormat trackFormat = trackFormatList.get(0);
+            Long durationUs = trackFormat.format.getLong(MediaFormat.KEY_DURATION);
+            // we need duration by milli for trickplay notification.
+            return durationUs != null ? durationUs / 1000 : null;
+        }
+        Log.e(TAG, "meta file for recording was not found: " + recordingId);
+        return null;
+    }
+
+    @MainThread
+    public long getCurrentPosition() {
+        // TODO: More precise time may be necessary.
+        MpegTsPlayer mpegTsPlayer = mPlayer;
+        long currentTime =
+                mpegTsPlayer != null
+                        ? mRecordStartTimeMs + mpegTsPlayer.getCurrentPosition()
+                        : mRecordStartTimeMs;
+        if (mChannel == null && mPlayerState == ExoPlayer.STATE_ENDED) {
+            currentTime = mRecordingDuration + mRecordStartTimeMs;
+        }
+        if (DEBUG) {
+            long systemCurrentTime = System.currentTimeMillis();
+            Log.d(
+                    TAG,
+                    "currentTime = "
+                            + currentTime
+                            + " ; System.currentTimeMillis() = "
+                            + systemCurrentTime
+                            + " ; diff = "
+                            + (currentTime - systemCurrentTime));
+        }
+        return currentTime;
+    }
+
+    @AnyThread
+    public void sendMessage(int messageType) {
+        mHandler.sendEmptyMessage(messageType);
+    }
+
+    @AnyThread
+    public void sendMessage(int messageType, Object object) {
+        mHandler.obtainMessage(messageType, object).sendToTarget();
+    }
+
+    @AnyThread
+    public void sendMessage(int messageType, int arg1, int arg2, Object object) {
+        mHandler.obtainMessage(messageType, arg1, arg2, object).sendToTarget();
+    }
+
+    @MainThread
+    public void release() {
+        if (DEBUG) {
+            Log.d(TAG, "release()");
+        }
+        synchronized (mReleaseLock) {
+            mReleaseRequested = true;
+        }
+        if (mHasSoftwareAudioDecoder) {
+            // TODO reimplement for google3
+            // Here disconnect ffmpeg
+        }
+        mAudioCapabilitiesReceiver.unregister();
+        mChannelDataManager.setListener(null);
+        mHandler.removeCallbacksAndMessages(null);
+        mHandler.sendEmptyMessage(MSG_RELEASE);
+    }
+
+    // MpegTsPlayer.Listener
+    // Called in the same thread as mHandler.
+    @Override
+    public void onStateChanged(boolean playWhenReady, int playbackState) {
+        if (DEBUG) {
+            Log.d(TAG, "ExoPlayer state change: " + playbackState + " " + playWhenReady);
+        }
+        if (playbackState == mPlayerState) {
+            return;
+        }
+        mReadyStartTimeMs = INVALID_TIME;
+        mPreparingStartTimeMs = INVALID_TIME;
+        mBufferingStartTimeMs = INVALID_TIME;
+        if (playbackState == ExoPlayer.STATE_READY) {
+            if (DEBUG) {
+                Log.d(TAG, "ExoPlayer ready");
+            }
+            if (!mPlayerStarted) {
+                sendMessage(MSG_START_PLAYBACK, System.identityHashCode(mPlayer));
+            }
+            mReadyStartTimeMs = SystemClock.elapsedRealtime();
+        } else if (playbackState == ExoPlayer.STATE_PREPARING) {
+            mPreparingStartTimeMs = SystemClock.elapsedRealtime();
+        } else if (playbackState == ExoPlayer.STATE_BUFFERING) {
+            mBufferingStartTimeMs = SystemClock.elapsedRealtime();
+        } else if (playbackState == ExoPlayer.STATE_ENDED) {
+            // Final status
+            // notification of STATE_ENDED from MpegTsPlayer will be ignored afterwards.
+            Log.i(TAG, "Player ended: end of stream");
+            if (mChannel != null) {
+                sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer));
+            }
+        }
+        mPlayerState = playbackState;
+    }
+
+    @Override
+    public void onError(Exception e) {
+        if (TunerPreferences.getStoreTsStream(mContext)) {
+            // Crash intentionally to capture the error causing TS file.
+            Log.e(
+                    TAG,
+                    "Crash intentionally to capture the error causing TS file. " + e.getMessage());
+            SoftPreconditions.checkState(false);
+        }
+        // There maybe some errors that finally raise ExoPlaybackException and will be handled here.
+        // If we are playing live stream, retrying playback maybe helpful. But for recorded stream,
+        // retrying playback is not helpful.
+        if (mChannel != null) {
+            mHandler.obtainMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer))
+                    .sendToTarget();
+        }
+    }
+
+    @Override
+    public void onVideoSizeChanged(int width, int height, float pixelWidthHeight) {
+        if (mChannel != null && mChannel.hasVideo()) {
+            updateVideoTrack(width, height);
+        }
+        if (mRecordingId != null) {
+            updateVideoTrack(width, height);
+        }
+    }
+
+    @Override
+    public void onDrawnToSurface(MpegTsPlayer player, Surface surface) {
+        if (mSurface != null && mPlayerStarted) {
+            if (DEBUG) {
+                Log.d(TAG, "MSG_DRAWN_TO_SURFACE");
+            }
+            if (mRecordingId != null) {
+                // Workaround of b/33298048: set it to 1 instead of 0.
+                mBufferStartTimeMs = mRecordStartTimeMs = 1;
+            } else {
+                mBufferStartTimeMs = mRecordStartTimeMs = System.currentTimeMillis();
+            }
+            notifyVideoAvailable();
+            mReportedDrawnToSurface = true;
+
+            // If surface is drawn successfully, it means that the playback was brought back
+            // to normal and therefore, the playback recovery status will be reset through
+            // setting a zero value to the retry count.
+            // TODO: Consider audio only channels for detecting playback status changes to
+            //       be normal.
+            mRetryCount = 0;
+            if (mCaptionEnabled && mCaptionTrack != null) {
+                startCaptionTrack();
+            } else {
+                stopCaptionTrack();
+            }
+            mHandler.sendEmptyMessage(MSG_NOTIFY_AUDIO_TRACK_UPDATED);
+        }
+    }
+
+    @Override
+    public void onSmoothTrickplayForceStopped() {
+        if (mPlayer == null || !mHandler.hasMessages(MSG_SMOOTH_TRICKPLAY_MONITOR)) {
+            return;
+        }
+        mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR);
+        doTrickplayBySeek((int) mPlayer.getCurrentPosition());
+    }
+
+    @Override
+    public void onAudioUnplayable() {
+        if (mPlayer == null) {
+            return;
+        }
+        Log.i(TAG, "AC3 audio cannot be played due to device limitation");
+        mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_SHOW_AUDIO_UNPLAYABLE);
+    }
+
+    // MpegTsPlayer.VideoEventListener
+    @Override
+    public void onEmitCaptionEvent(Cea708Data.CaptionEvent event) {
+        mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_PROCESS_CAPTION_TRACK, event);
+    }
+
+    @Override
+    public void onClearCaptionEvent() {
+        mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_CLEAR_CAPTION_RENDERER);
+    }
+
+    @Override
+    public void onDiscoverCaptionServiceNumber(int serviceNumber) {
+        sendMessage(MSG_DISCOVER_CAPTION_SERVICE_NUMBER, serviceNumber);
+    }
+
+    // ChannelDataManager.ProgramInfoListener
+    @Override
+    public void onProgramsArrived(TunerChannel channel, List<EitItem> programs) {
+        sendMessage(MSG_SCHEDULE_OF_PROGRAMS, new Pair<>(channel, programs));
+    }
+
+    @Override
+    public void onChannelArrived(TunerChannel channel) {
+        sendMessage(MSG_UPDATE_CHANNEL_INFO, channel);
+    }
+
+    @Override
+    public void onRescanNeeded() {
+        mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_TOAST_RESCAN_NEEDED);
+    }
+
+    @Override
+    public void onRequestProgramsResponse(TunerChannel channel, List<EitItem> programs) {
+        sendMessage(MSG_PROGRAM_DATA_RESULT, new Pair<>(channel, programs));
+    }
+
+    // PlaybackBufferListener
+    @Override
+    public void onBufferStartTimeChanged(long startTimeMs) {
+        sendMessage(MSG_BUFFER_START_TIME_CHANGED, startTimeMs);
+    }
+
+    @Override
+    public void onBufferStateChanged(boolean available) {
+        sendMessage(MSG_BUFFER_STATE_CHANGED, available);
+    }
+
+    @Override
+    public void onDiskTooSlow() {
+        mTrickplayDisabledByStorageIssue = true;
+        sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer));
+    }
+
+    // EventDetector.EventListener
+    @Override
+    public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) {
+        mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime);
+    }
+
+    @Override
+    public void onEventDetected(TunerChannel channel, List<EitItem> items) {
+        mChannelDataManager.notifyEventDetected(channel, items);
+    }
+
+    @Override
+    public void onChannelScanDone() {
+        // do nothing.
+    }
+
+    private long parseChannel(Uri uri) {
+        try {
+            List<String> paths = uri.getPathSegments();
+            if (paths.size() > 1 && paths.get(0).equals(PLAY_FROM_CHANNEL)) {
+                return ContentUris.parseId(uri);
+            }
+        } catch (UnsupportedOperationException | NumberFormatException e) {
+        }
+        return -1;
+    }
+
+    private static class RecordedProgram {
+        //        private final long mChannelId;
+        private final String mDataUri;
+        private final long mStartTimeMillis;
+
+        private static final String[] PROJECTION = {
+            TvContract.Programs.COLUMN_CHANNEL_ID,
+            TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI,
+            TvContract.RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS,
+        };
+
+        public RecordedProgram(Cursor cursor) {
+            int index = 0;
+            //            mChannelId = cursor.getLong(index++);
+            index++;
+            mDataUri = cursor.getString(index++);
+            mStartTimeMillis = cursor.getLong(index++);
+        }
+
+        public RecordedProgram(long channelId, String dataUri) {
+            //            mChannelId = channelId;
+            mDataUri = dataUri;
+            mStartTimeMillis = 0;
+        }
+
+        public static RecordedProgram onQuery(Cursor c) {
+            RecordedProgram recording = null;
+            if (c != null && c.moveToNext()) {
+                recording = new RecordedProgram(c);
+            }
+            return recording;
+        }
+
+        public String getDataUri() {
+            return mDataUri;
+        }
+
+        public long getStartTime() {
+            return mStartTimeMillis;
+        }
+    }
+
+    private RecordedProgram getRecordedProgram(Uri recordedUri) {
+        ContentResolver resolver = mContext.getContentResolver();
+        try (Cursor c = resolver.query(recordedUri, RecordedProgram.PROJECTION, null, null, null)) {
+            if (c != null) {
+                RecordedProgram result = RecordedProgram.onQuery(c);
+                if (DEBUG) {
+                    Log.d(TAG, "Finished query for " + this);
+                }
+                return result;
+            } else {
+                if (c == null) {
+                    Log.e(TAG, "Unknown query error for " + this);
+                } else {
+                    if (DEBUG) {
+                        Log.d(TAG, "Canceled query for " + this);
+                    }
+                }
+                return null;
+            }
+        }
+    }
+
+    private String parseRecording(Uri uri) {
+        RecordedProgram recording = getRecordedProgram(uri);
+        if (recording != null) {
+            mRecordedProgramStartTimeMs = recording.getStartTime();
+            return recording.getDataUri();
+        }
+        return null;
+    }
+
+    @Override
+    public boolean handleMessage(Message msg) {
+        switch (msg.what) {
+            case MSG_TUNE:
+                return handleMessageTune((Uri) msg.obj);
+            case MSG_STOP_TUNE:
+                return handleMessageStopTune();
+            case MSG_RELEASE:
+                return handleMessageRelease();
+            case MSG_RETRY_PLAYBACK:
+                return handleMessageRetryPlayback((int) msg.obj);
+            case MSG_RESET_PLAYBACK:
+                return handleMessageResetPlayback();
+            case MSG_START_PLAYBACK:
+                return handleMessageStartPlayback((int) msg.obj);
+            case MSG_UPDATE_PROGRAM:
+                return handleMessageUpdateProgram((EitItem) msg.obj);
+            case MSG_SCHEDULE_OF_PROGRAMS:
+                // TODO: fix the unchecked cast waring.
+                Pair<TunerChannel, List<EitItem>> pair =
+                        (Pair<TunerChannel, List<EitItem>>) msg.obj;
+                return handleMessageScheduleOfPrograms(pair);
+            case MSG_UPDATE_CHANNEL_INFO:
+                return handleMessageUpdateChannelInfo((TunerChannel) msg.obj);
+            case MSG_PROGRAM_DATA_RESULT:
+                return handleMessageProgramDataResult(msg);
+            case MSG_TRICKPLAY_BY_SEEK:
+                return handleMessageTrickplayBySeek(msg.arg1);
+            case MSG_SMOOTH_TRICKPLAY_MONITOR:
+                return handleMessageSmoothTrickplayMonitor();
+            case MSG_RESCHEDULE_PROGRAMS:
+                return handleMessageReschedulePrograms();
+            case MSG_PARENTAL_CONTROLS:
+                return handleMessageParentalControl();
+            case MSG_UNBLOCKED_RATING:
+                return handleMessageUnblockedRating((TvContentRating) msg.obj);
+            case MSG_DISCOVER_CAPTION_SERVICE_NUMBER:
+                return handleMessageDiscoverCaptionServiceNumber((int) msg.obj);
+            case MSG_SELECT_TRACK:
+                return handleMessageSelectTrack(msg.arg1, (String) msg.obj);
+            case MSG_UPDATE_CAPTION_TRACK:
+                return handleMessageUpdateCaptionTrack();
+            case MSG_TIMESHIFT_PAUSE:
+                return handleMessageTimeshiftPause();
+            case MSG_TIMESHIFT_RESUME:
+                return handleMessageTimeshiftResume();
+            case MSG_TIMESHIFT_SEEK_TO:
+                return handleMessageTimeshiftSeekTo((long) msg.obj);
+            case MSG_TIMESHIFT_SET_PLAYBACKPARAMS:
+                return handleMessageTimeshiftSetPlaybackParams((PlaybackParams) msg.obj);
+            case MSG_SET_STREAM_VOLUME:
+                return handleMessageSetStreamVolume();
+            case MSG_TUNER_PREFERENCES_CHANGED:
+                return handleMessageTunerPreferencesChanged();
+            case MSG_BUFFER_START_TIME_CHANGED:
+                return handleMessageBufferStartTimeChanged((long) msg.obj);
+            case MSG_BUFFER_STATE_CHANGED:
+                return handleMessageBufferStateChanged((boolean) msg.obj);
+            case MSG_CHECK_SIGNAL:
+                return handleMessageCheckSignal();
+            case MSG_SET_SURFACE:
+                return handleMessageSetSurface();
+            case MSG_NOTIFY_AUDIO_TRACK_UPDATED:
+                return handleMessageAudioTrackUpdated();
+            case MSG_CHECK_SIGNAL_STRENGTH:
+                return handleMessageCheckSignalStrength();
+            default:
+                return unhandledMessage(msg);
+        }
+    }
+
+    private boolean handleMessageTune(Uri channelUri) {
+        if (DEBUG) {
+            Log.d(TAG, "MSG_TUNE");
+        }
+
+        // When sequential tuning messages arrived, it skips middle tuning messages in
+        // order
+        // to change to the last requested channel quickly.
+        if (mHandler.hasMessages(MSG_TUNE)) {
+            return true;
+        }
+        notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
+        if (!mIsActiveSession) {
+            // Wait until release is finished if there is a pending release.
+            try {
+                while (!sActiveSessionSemaphore.tryAcquire(
+                        RELEASE_WAIT_INTERVAL_MS, TimeUnit.MILLISECONDS)) {
+                    synchronized (mReleaseLock) {
+                        if (mReleaseRequested) {
+                            return true;
+                        }
+                    }
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+            synchronized (mReleaseLock) {
+                if (mReleaseRequested) {
+                    sActiveSessionSemaphore.release();
+                    return true;
+                }
+            }
+            mIsActiveSession = true;
+        }
+        String recording = null;
+        long channelId = parseChannel(channelUri);
+        TunerChannel channel = (channelId == -1) ? null : mChannelDataManager.getChannel(channelId);
+        if (channelId == -1) {
+            recording = parseRecording(channelUri);
+        }
+        if (channel == null && recording == null) {
+            Log.w(TAG, "onTune() is failed. Can't find channel for " + channelUri);
+            stopTune();
+            notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+            return true;
+        }
+        clearCallbacksAndMessagesSafely();
+        mChannelDataManager.removeAllCallbacksAndMessages();
+        if (channel != null) {
+            mChannelDataManager.requestProgramsData(channel);
+        }
+        prepareTune(channel, recording);
+        // TODO: Need to refactor. notifyContentAllowed() should not be called if
+        // parental
+        // control is turned on.
+        mSession.notifyContentAllowed();
+        resetTvTracks();
+        resetPlayback();
+        mHandler.sendEmptyMessageDelayed(
+                MSG_RESCHEDULE_PROGRAMS, RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS);
+        return true;
+    }
+
+    private boolean handleMessageStopTune() {
+        if (DEBUG) {
+            Log.d(TAG, "MSG_STOP_TUNE");
+        }
+        mChannel = null;
+        stopPlayback(true);
+        stopCaptionTrack();
+        resetTvTracks();
+        notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+        return true;
+    }
+
+    private boolean handleMessageRelease() {
+        if (DEBUG) {
+            Log.d(TAG, "MSG_RELEASE");
+        }
+        mHandler.removeCallbacksAndMessages(null);
+        stopPlayback(true);
+        stopCaptionTrack();
+        mSourceManager.release();
+        mHandler.getLooper().quitSafely();
+        if (mIsActiveSession) {
+            sActiveSessionSemaphore.release();
+        }
+        return true;
+    }
+
+    private boolean handleMessageRetryPlayback(int code) {
+        if (System.identityHashCode(mPlayer) == code) {
+            Log.i(TAG, "Retrying the playback for channel: " + mChannel);
+            mHandler.removeMessages(MSG_RETRY_PLAYBACK);
+            // When there is a request of retrying playback, don't reuse TunerHal.
+            mSourceManager.setKeepTuneStatus(false);
+            mRetryCount++;
+            if (DEBUG) {
+                Log.d(TAG, "MSG_RETRY_PLAYBACK " + mRetryCount);
+            }
+            mChannelDataManager.removeAllCallbacksAndMessages();
+            if (mRetryCount <= MAX_IMMEDIATE_RETRY_COUNT) {
+                resetPlayback();
+            } else {
+                // When it reaches this point, it may be due to an error that occurred
+                // in
+                // the tuner device. Calling stopPlayback() resets the tuner device
+                // to recover from the error.
+                stopPlayback(false);
+                stopCaptionTrack();
+
+                notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
+                Log.i(TAG, "Notify weak signal since fail to retry playback");
+
+                // After MAX_IMMEDIATE_RETRY_COUNT, give some delay of an empirically
+                // chosen
+                // value before recovering the playback.
+                mHandler.sendEmptyMessageDelayed(
+                        MSG_RESET_PLAYBACK, RECOVER_STOPPED_PLAYBACK_PERIOD_MS);
+            }
+        }
+        return true;
+    }
+
+    private boolean handleMessageResetPlayback() {
+        if (DEBUG) {
+            Log.d(TAG, "MSG_RESET_PLAYBACK");
+        }
+        mChannelDataManager.removeAllCallbacksAndMessages();
+        resetPlayback();
+        return true;
+    }
+
+    private boolean handleMessageStartPlayback(int playerHashCode) {
+        if (DEBUG) {
+            Log.d(TAG, "MSG_START_PLAYBACK");
+        }
+        if (mChannel != null || mRecordingId != null) {
+            startPlayback(playerHashCode);
+        }
+        return true;
+    }
+
+    private boolean handleMessageUpdateProgram(EitItem program) {
+        if (mChannel != null) {
+            updateTvTracks(program, false);
+            mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
+        }
+        return true;
+    }
+
+    private boolean handleMessageScheduleOfPrograms(Pair<TunerChannel, List<EitItem>> pair) {
+        mHandler.removeMessages(MSG_UPDATE_PROGRAM);
+        TunerChannel channel = pair.first;
+        if (mChannel == null) {
+            return true;
+        }
+        if (mChannel != null && mChannel.compareTo(channel) != 0) {
+            return true;
+        }
+        mPrograms = pair.second;
+        EitItem currentProgram = getCurrentProgram();
+        if (currentProgram == null) {
+            mProgram = null;
+        }
+        long currentTimeMs = getCurrentPosition();
+        if (mPrograms != null) {
+            for (EitItem item : mPrograms) {
+                if (currentProgram != null && currentProgram.compareTo(item) == 0) {
+                    if (DEBUG) {
+                        Log.d(TAG, "Update current TvTracks " + item);
+                    }
+                    if (mProgram != null && mProgram.compareTo(item) == 0) {
+                        continue;
+                    }
+                    mProgram = item;
+                    updateTvTracks(item, false);
+                } else if (item.getStartTimeUtcMillis() > currentTimeMs) {
+                    if (DEBUG) {
+                        Log.d(
+                                TAG,
+                                "Update next TvTracks "
+                                        + item
+                                        + " "
+                                        + (item.getStartTimeUtcMillis() - currentTimeMs));
+                    }
+                    mHandler.sendMessageDelayed(
+                            mHandler.obtainMessage(MSG_UPDATE_PROGRAM, item),
+                            item.getStartTimeUtcMillis() - currentTimeMs);
+                }
+            }
+        }
+        mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
+        return true;
+    }
+
+    private boolean handleMessageUpdateChannelInfo(TunerChannel tunerChannel) {
+        if (mChannel != null && mChannel.compareTo(tunerChannel) == 0) {
+            updateChannelInfo(tunerChannel);
+        }
+        return true;
+    }
+
+    private boolean handleMessageProgramDataResult(Message msg) {
+        TunerChannel channel = (TunerChannel) ((Pair) msg.obj).first;
+
+        // If there already exists, skip it since real-time data is a top priority,
+        if (mChannel != null
+                && mChannel.compareTo(channel) == 0
+                && mPrograms == null
+                && mProgram == null) {
+            sendMessage(MSG_SCHEDULE_OF_PROGRAMS, msg.obj);
+        }
+        return true;
+    }
+
+    private boolean handleMessageTrickplayBySeek(int seekPositionMs) {
+        if (mPlayer == null) {
+            return true;
+        }
+        if (mRecordingId != null) {
+            long systemBufferTime =
+                    System.currentTimeMillis() - SEEK_MARGIN_MS - mRecordedProgramStartTimeMs;
+            if (seekPositionMs > systemBufferTime) {
+                doTimeShiftResume();
+                return true;
+            }
+        }
+        doTrickplayBySeek(seekPositionMs);
+        return true;
+    }
+
+    private boolean handleMessageSmoothTrickplayMonitor() {
+        if (mPlayer == null) {
+            return true;
+        }
+        long systemCurrentTime = System.currentTimeMillis();
+        long position = getCurrentPosition();
+        if (mRecordingId == null) {
+            // Checks if the position exceeds the upper bound when forwarding,
+            // or exceed the lower bound when rewinding.
+            // If the direction is not checked, there can be some issues.
+            // (See b/29939781 for more details.)
+            if ((position > systemCurrentTime && mPlaybackParams.getSpeed() > 0L)
+                    || (position < mBufferStartTimeMs && mPlaybackParams.getSpeed() < 0L)) {
+                doTimeShiftResume();
+                return true;
+            }
+        } else {
+            if (position > mRecordingDuration || position < 0) {
+                doTimeShiftPause();
+                return true;
+            }
+            long systemBufferTime =
+                    systemCurrentTime - SEEK_MARGIN_MS - mRecordedProgramStartTimeMs;
+            if (position > systemBufferTime) {
+                doTimeShiftResume();
+                return true;
+            }
+        }
+        mHandler.sendEmptyMessageDelayed(
+                MSG_SMOOTH_TRICKPLAY_MONITOR, TRICKPLAY_MONITOR_INTERVAL_MS);
+        return true;
+    }
+
+    private boolean handleMessageReschedulePrograms() {
+        if (mHandler.hasMessages(MSG_SCHEDULE_OF_PROGRAMS)) {
+            mHandler.sendEmptyMessage(MSG_RESCHEDULE_PROGRAMS);
+        } else {
+            doReschedulePrograms();
+        }
+        return true;
+    }
+
+    private boolean handleMessageParentalControl() {
+        doParentalControls();
+        mHandler.removeMessages(MSG_PARENTAL_CONTROLS);
+        mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS);
+        return true;
+    }
+
+    private boolean handleMessageUnblockedRating(TvContentRating unblockedContentRating) {
+        mUnblockedContentRating = unblockedContentRating;
+        return handleMessageParentalControl();
+    }
+
+    private boolean handleMessageDiscoverCaptionServiceNumber(int serviceNumber) {
+        doDiscoverCaptionServiceNumber(serviceNumber);
+        return true;
+    }
+
+    private boolean handleMessageSelectTrack(int type, String trackId) {
+        if (mPlayer == null) {
+            Log.w(TAG, "mPlayer is null when doselectTrack is called");
+            return false;
+        }
+        if (mChannel != null || mRecordingId != null) {
+            doSelectTrack(type, trackId);
+        }
+        return true;
+    }
+
+    private boolean handleMessageUpdateCaptionTrack() {
+        if (mCaptionEnabled) {
+            startCaptionTrack();
+        } else {
+            stopCaptionTrack();
+        }
+        return true;
+    }
+
+    private boolean handleMessageTimeshiftPause() {
+        if (DEBUG) {
+            Log.d(TAG, "MSG_TIMESHIFT_PAUSE");
+        }
+        if (mPlayer == null) {
+            return true;
+        }
+        setTrickplayEnabledIfNeeded();
+        doTimeShiftPause();
+        return true;
+    }
+
+    private boolean handleMessageTimeshiftResume() {
+        if (DEBUG) {
+            Log.d(TAG, "MSG_TIMESHIFT_RESUME");
+        }
+        if (mPlayer == null) {
+            return true;
+        }
+        setTrickplayEnabledIfNeeded();
+        doTimeShiftResume();
+        return true;
+    }
+
+    private boolean handleMessageTimeshiftSeekTo(long timeMs) {
+        if (DEBUG) {
+            Log.d(TAG, "MSG_TIMESHIFT_SEEK_TO (position=" + timeMs + ")");
+        }
+        if (mPlayer == null) {
+            return true;
+        }
+        setTrickplayEnabledIfNeeded();
+        doTimeShiftSeekTo(timeMs);
+        return true;
+    }
+
+    private boolean handleMessageTimeshiftSetPlaybackParams(PlaybackParams playbackParams) {
+        if (mPlayer == null) {
+            return true;
+        }
+        setTrickplayEnabledIfNeeded();
+        doTimeShiftSetPlaybackParams(playbackParams);
+        return true;
+    }
+
+    private boolean handleMessageAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) {
+        if (DEBUG) {
+            Log.d(TAG, "MSG_AUDIO_CAPABILITIES_CHANGED " + audioCapabilities);
+        }
+        if (audioCapabilities == null) {
+            return true;
+        }
+        if (!audioCapabilities.equals(mAudioCapabilities)) {
+            // HDMI supported encodings are changed. restart player.
+            mAudioCapabilities = audioCapabilities;
+            resetPlayback();
+        }
+        return true;
+    }
+
+    private boolean handleMessageSetStreamVolume() {
+        if (mPlayer != null && mPlayer.isPlaying()) {
+            mPlayer.setVolume(mVolume);
+        }
+        return true;
+    }
+
+    private boolean handleMessageTunerPreferencesChanged() {
+        mHandler.removeMessages(MSG_TUNER_PREFERENCES_CHANGED);
+        @TrickplaySetting int trickplaySetting = TunerPreferences.getTrickplaySetting(mContext);
+        if (trickplaySetting != mTrickplaySetting) {
+            boolean wasTrcikplayEnabled =
+                    mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED;
+            boolean isTrickplayEnabled =
+                    trickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED;
+            mTrickplaySetting = trickplaySetting;
+            if (isTrickplayEnabled != wasTrcikplayEnabled) {
+                sendMessage(MSG_RESET_PLAYBACK, System.identityHashCode(mPlayer));
+            }
+        }
+        return true;
+    }
+
+    private boolean handleMessageBufferStartTimeChanged(long bufferStartTimeMs) {
+        if (mPlayer == null) {
+            return true;
+        }
+        mBufferStartTimeMs = bufferStartTimeMs;
+        if (!hasEnoughBackwardBuffer()
+                && (!mPlayer.isPlaying() || mPlaybackParams.getSpeed() < 1.0f)) {
+            mPlayer.setPlayWhenReady(true);
+            mPlayer.setAudioTrackAndClosedCaption(true);
+            mPlaybackParams.setSpeed(1.0f);
+        }
+        return true;
+    }
+
+    private boolean handleMessageBufferStateChanged(boolean available) {
+        mSession.notifyTimeShiftStatusChanged(
+                available
+                        ? TvInputManager.TIME_SHIFT_STATUS_AVAILABLE
+                        : TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE);
+        return true;
+    }
+
+    private boolean handleMessageCheckSignal() {
+        if (mChannel == null || mPlayer == null) {
+            return true;
+        }
+        TsDataSource source = mPlayer.getDataSource();
+        long limitInBytes = source != null ? source.getBufferedPosition() : 0L;
+        if (TunerDebug.ENABLED) {
+            TunerDebug.calculateDiff();
+            mTunerSessionOverlay.sendUiMessage(
+                    TunerSessionOverlay.MSG_UI_SET_STATUS_TEXT,
+                    Html.fromHtml(
+                            StatusTextUtils.getStatusWarningInHTML(
+                                    (limitInBytes - mLastLimitInBytes) / TS_PACKET_SIZE,
+                                    TunerDebug.getVideoFrameDrop(),
+                                    TunerDebug.getBytesInQueue(),
+                                    TunerDebug.getAudioPositionUs(),
+                                    TunerDebug.getAudioPositionUsRate(),
+                                    TunerDebug.getAudioPtsUs(),
+                                    TunerDebug.getAudioPtsUsRate(),
+                                    TunerDebug.getVideoPtsUs(),
+                                    TunerDebug.getVideoPtsUsRate())));
+        }
+        mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_HIDE_MESSAGE);
+        long currentTime = SystemClock.elapsedRealtime();
+        long bufferingTimeMs =
+                mBufferingStartTimeMs != INVALID_TIME
+                        ? currentTime - mBufferingStartTimeMs
+                        : mBufferingStartTimeMs;
+        long preparingTimeMs =
+                mPreparingStartTimeMs != INVALID_TIME
+                        ? currentTime - mPreparingStartTimeMs
+                        : mPreparingStartTimeMs;
+        boolean isBufferingTooLong = bufferingTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
+        boolean isPreparingTooLong = preparingTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
+        boolean isWeakSignal =
+                source != null
+                        && mChannel.getType() != Channel.TunerType.TYPE_FILE
+                        && (isBufferingTooLong || isPreparingTooLong);
+        if (isWeakSignal && !mReportedWeakSignal) {
+            if (!mHandler.hasMessages(MSG_RETRY_PLAYBACK)) {
+                mHandler.sendMessageDelayed(
+                        mHandler.obtainMessage(
+                                MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)),
+                        PLAYBACK_RETRY_DELAY_MS);
+            }
+            if (mPlayer != null) {
+                mPlayer.setAudioTrackAndClosedCaption(false);
+            }
+            notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
+            if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) {
+                mSession.notifySignalStrength(0);
+                mHandler.removeMessages(MSG_CHECK_SIGNAL_STRENGTH);
+            }
+            Log.i(
+                    TAG,
+                    "Notify weak signal due to signal check, "
+                            + String.format(
+                                    "packetsPerSec:%d, bufferingTimeMs:%d, preparingTimeMs:%d, "
+                                            + "videoFrameDrop:%d",
+                                    (limitInBytes - mLastLimitInBytes) / TS_PACKET_SIZE,
+                                    bufferingTimeMs,
+                                    preparingTimeMs,
+                                    TunerDebug.getVideoFrameDrop()));
+        } else if (!isWeakSignal && mReportedWeakSignal) {
+            boolean isPlaybackStable =
+                    mReadyStartTimeMs != INVALID_TIME
+                            && currentTime - mReadyStartTimeMs
+                                    > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
+            if (!isPlaybackStable) {
+                // Wait until playback becomes stable.
+            } else if (mReportedDrawnToSurface) {
+                mHandler.removeMessages(MSG_RETRY_PLAYBACK);
+                notifyVideoAvailable();
+                mPlayer.setAudioTrackAndClosedCaption(true);
+                if (mHandler.hasMessages(MSG_CHECK_SIGNAL_STRENGTH)) {
+                    mHandler.removeMessages(MSG_CHECK_SIGNAL_STRENGTH);
+                }
+                if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) {
+                    sendMessage(MSG_CHECK_SIGNAL_STRENGTH);
+                }
+            }
+        }
+        mLastLimitInBytes = limitInBytes;
+        mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_PERIOD_MS);
+        return true;
+    }
+
+    private boolean handleMessageSetSurface() {
+        if (mPlayer != null) {
+            mPlayer.setSurface(mSurface);
+        } else {
+            // TODO: Since surface is dynamically set, we can remove the dependency of
+            // playback start on mSurface nullity.
+            resetPlayback();
+        }
+        return true;
+    }
+
+    private boolean handleMessageAudioTrackUpdated() {
+        notifyAudioTracksUpdated();
+        return true;
+    }
+
+    private boolean handleMessageCheckSignalStrength() {
+        if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) {
+            int signal;
+            if (mPlayer != null) {
+                TsDataSource source = mPlayer.getDataSource();
+                if (source != null) {
+                    signal = source.getSignalStrength();
+                    return handleSignal(signal);
+                }
+            }
+        }
+        return false;
+    }
+
+    @VisibleForTesting
+    protected boolean handleSignal(int signal) {
+        if (signal == TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED
+                || signal == TvInputConstantCompat.SIGNAL_STRENGTH_ERROR) {
+            notifySignal(signal);
+            return true;
+        }
+        if (signal != mSignalStrength && signal >= 0) {
+            notifySignal(signal);
+        }
+        mHandler.sendEmptyMessageDelayed(
+                MSG_CHECK_SIGNAL_STRENGTH, CHECK_SIGNAL_STRENGTH_INTERVAL_MS);
+        return true;
+    }
+
+    @VisibleForTesting
+    protected void notifySignal(int signal) {
+        mSession.notifySignalStrength(signal);
+        mSignalStrength = signal;
+    }
+
+    private boolean unhandledMessage(Message msg) {
+        Log.w(TAG, "Unhandled message code: " + msg.what);
+        return false;
+    }
+
+    // Private methods
+    private void doSelectTrack(int type, String trackId) {
+        int numTrackId =
+                trackId != null ? Integer.parseInt(trackId.substring(TRACK_PREFIX_SIZE)) : -1;
+        if (type == TvTrackInfo.TYPE_AUDIO) {
+            if (trackId == null) {
+                return;
+            }
+            if (numTrackId != mPlayer.getSelectedTrack(MpegTsPlayer.TRACK_TYPE_AUDIO)) {
+                mPlayer.setSelectedTrack(MpegTsPlayer.TRACK_TYPE_AUDIO, numTrackId);
+            }
+            mSession.notifyTrackSelected(type, trackId);
+        } else if (type == TvTrackInfo.TYPE_SUBTITLE) {
+            if (trackId == null) {
+                mSession.notifyTrackSelected(type, null);
+                mCaptionTrack = null;
+                stopCaptionTrack();
+                return;
+            }
+            for (TvTrackInfo track : mTvTracks) {
+                if (track.getId().equals(trackId)) {
+                    // The service number of the caption service is used for track id of a
+                    // subtitle track. Passes the following track id on to TsParser.
+                    mSession.notifyTrackSelected(type, trackId);
+                    mCaptionTrack = mCaptionTrackMap.get(numTrackId);
+                    startCaptionTrack();
+                    return;
+                }
+            }
+        }
+    }
+
+    private void setTrickplayEnabledIfNeeded() {
+        if (mChannel == null
+                || mTrickplayModeCustomization != CustomizationManager.TRICKPLAY_MODE_ENABLED) {
+            return;
+        }
+        if (mTrickplaySetting == TunerPreferences.TRICKPLAY_SETTING_NOT_SET) {
+            mTrickplaySetting = TunerPreferences.TRICKPLAY_SETTING_ENABLED;
+            TunerPreferences.setTrickplaySetting(mContext, mTrickplaySetting);
+        }
+    }
+
+    @VisibleForTesting
+    protected MpegTsPlayer createPlayer(AudioCapabilities capabilities) {
+        if (capabilities == null) {
+            Log.w(TAG, "No Audio Capabilities");
+        }
+        mSourceManager.setKeepTuneStatus(true);
+        long now = System.currentTimeMillis();
+        if (mTrickplayModeCustomization == CustomizationManager.TRICKPLAY_MODE_ENABLED
+                && mTrickplaySetting == TunerPreferences.TRICKPLAY_SETTING_NOT_SET) {
+            if (mTrickplayExpiredMs == 0) {
+                mTrickplayExpiredMs = now + TRICKPLAY_OFF_DURATION_MS;
+                TunerPreferences.setTrickplayExpiredMs(mContext, mTrickplayExpiredMs);
+            } else {
+                if (mTrickplayExpiredMs < now) {
+                    mTrickplaySetting = TunerPreferences.TRICKPLAY_SETTING_DISABLED;
+                    TunerPreferences.setTrickplaySetting(mContext, mTrickplaySetting);
+                }
+            }
+        }
+        BufferManager bufferManager = null;
+        if (mRecordingId != null) {
+            StorageManager storageManager =
+                    new DvrStorageManager(new File(getRecordingPath()), false);
+            bufferManager = new BufferManager(storageManager);
+            updateCaptionTracks(((DvrStorageManager) storageManager).readCaptionInfoFiles());
+        } else if (!mTrickplayDisabledByStorageIssue
+                && mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED
+                && mMaxTrickplayBufferSizeMb >= MIN_BUFFER_SIZE_DEF) {
+            bufferManager =
+                    new BufferManager(
+                            new TrickplayStorageManager(
+                                    mContext,
+                                    mTrickplayBufferDir,
+                                    1024L * 1024 * mMaxTrickplayBufferSizeMb));
+        } else {
+            Log.w(TAG, "Trickplay is disabled.");
+        }
+        MpegTsPlayer player =
+                new MpegTsPlayer(
+                        new MpegTsRendererBuilder(
+                                mContext, bufferManager, this, mConcurrentDvrPlaybackFlags),
+                        mHandler,
+                        mSourceManager,
+                        capabilities,
+                        this);
+        Log.i(TAG, "Passthrough AC3 renderer");
+        if (DEBUG) {
+            Log.d(TAG, "ExoPlayer created");
+        }
+        player.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER);
+        player.setVideoEventListener(this);
+        player.setCaptionServiceNumber(
+                mCaptionTrack != null
+                        ? mCaptionTrack.serviceNumber
+                        : Cea708Data.EMPTY_SERVICE_NUMBER);
+        return player;
+    }
+
+    private void startCaptionTrack() {
+        if (mCaptionEnabled && mCaptionTrack != null) {
+            mTunerSessionOverlay.sendUiMessage(
+                    TunerSessionOverlay.MSG_UI_START_CAPTION_TRACK, mCaptionTrack);
+            if (mPlayer != null) {
+                mPlayer.setCaptionServiceNumber(mCaptionTrack.serviceNumber);
+            }
+        }
+    }
+
+    private void stopCaptionTrack() {
+        if (mPlayer != null) {
+            mPlayer.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER);
+        }
+        mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_STOP_CAPTION_TRACK);
+    }
+
+    private void resetTvTracks() {
+        mTvTracks.clear();
+        mAudioTrackMap.clear();
+        mCaptionTrackMap.clear();
+        mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_RESET_CAPTION_TRACK);
+        mSession.notifyTracksChanged(mTvTracks);
+    }
+
+    private void updateTvTracks(TvTracksInterface tvTracksInterface, boolean fromPmt) {
+        synchronized (tvTracksInterface) {
+            if (DEBUG) {
+                Log.d(TAG, "UpdateTvTracks " + tvTracksInterface);
+            }
+            List<AtscAudioTrack> audioTracks = tvTracksInterface.getAudioTracks();
+            List<AtscCaptionTrack> captionTracks = tvTracksInterface.getCaptionTracks();
+            // According to ATSC A/69 chapter 6.9, both PMT and EIT should have descriptors for
+            // audio
+            // tracks, but in real world, we see some bogus audio track info in EIT, so, we trust
+            // audio
+            // track info in PMT more and use info in EIT only when we have nothing.
+            if (audioTracks != null
+                    && !audioTracks.isEmpty()
+                    && (mChannel == null || mChannel.getAudioTracks() == null || fromPmt)) {
+                updateAudioTracks(audioTracks);
+            }
+            if (captionTracks == null || captionTracks.isEmpty()) {
+                if (tvTracksInterface.hasCaptionTrack()) {
+                    updateCaptionTracks(captionTracks);
+                }
+            } else {
+                updateCaptionTracks(captionTracks);
+            }
+        }
+    }
+
+    private void removeTvTracks(int trackType) {
+        Iterator<TvTrackInfo> iterator = mTvTracks.iterator();
+        while (iterator.hasNext()) {
+            TvTrackInfo tvTrackInfo = iterator.next();
+            if (tvTrackInfo.getType() == trackType) {
+                iterator.remove();
+            }
+        }
+    }
+
+    private void updateVideoTrack(int width, int height) {
+        removeTvTracks(TvTrackInfo.TYPE_VIDEO);
+        mTvTracks.add(
+                new TvTrackInfo.Builder(TvTrackInfo.TYPE_VIDEO, VIDEO_TRACK_ID)
+                        .setVideoWidth(width)
+                        .setVideoHeight(height)
+                        .build());
+        mSession.notifyTracksChanged(mTvTracks);
+        mSession.notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, VIDEO_TRACK_ID);
+    }
+
+    private void updateAudioTracks(List<AtscAudioTrack> audioTracks) {
+        if (DEBUG) {
+            Log.d(TAG, "Update AudioTracks " + audioTracks);
+        }
+        mAudioTrackMap.clear();
+        if (audioTracks != null) {
+            int index = 0;
+            for (AtscAudioTrack audioTrack : audioTracks) {
+                audioTrack.index = index;
+                mAudioTrackMap.put(index, audioTrack);
+                ++index;
+            }
+        }
+        mHandler.sendEmptyMessage(MSG_NOTIFY_AUDIO_TRACK_UPDATED);
+    }
+
+    private void notifyAudioTracksUpdated() {
+        if (mPlayer == null) {
+            // Audio tracks will be updated later once player initialization is done.
+            return;
+        }
+        int audioTrackCount = mPlayer.getTrackCount(MpegTsPlayer.TRACK_TYPE_AUDIO);
+        removeTvTracks(TvTrackInfo.TYPE_AUDIO);
+        for (int i = 0; i < audioTrackCount; i++) {
+            // We use language information from EIT/VCT only when the player does not provide
+            // languages.
+            com.google.android.exoplayer.MediaFormat infoFromPlayer =
+                    mPlayer.getTrackFormat(MpegTsPlayer.TRACK_TYPE_AUDIO, i);
+            AtscAudioTrack infoFromEit = mAudioTrackMap.get(i);
+            AtscAudioTrack infoFromVct =
+                    (mChannel != null
+                                    && mChannel.getAudioTracks().size() == mAudioTrackMap.size()
+                                    && i < mChannel.getAudioTracks().size())
+                            ? mChannel.getAudioTracks().get(i)
+                            : null;
+            String language =
+                    !TextUtils.isEmpty(infoFromPlayer.language)
+                            ? infoFromPlayer.language
+                            : (infoFromEit != null && infoFromEit.language != null)
+                                    ? infoFromEit.language
+                                    : (infoFromVct != null && infoFromVct.language != null)
+                                            ? infoFromVct.language
+                                            : null;
+            TvTrackInfo.Builder builder =
+                    new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, AUDIO_TRACK_PREFIX + i);
+            builder.setLanguage(language);
+            builder.setAudioChannelCount(infoFromPlayer.channelCount);
+            builder.setAudioSampleRate(infoFromPlayer.sampleRate);
+            TvTrackInfo track = builder.build();
+            mTvTracks.add(track);
+        }
+        mSession.notifyTracksChanged(mTvTracks);
+    }
+
+    private void updateCaptionTracks(List<AtscCaptionTrack> captionTracks) {
+        if (DEBUG) {
+            Log.d(TAG, "Update CaptionTrack " + captionTracks);
+        }
+        removeTvTracks(TvTrackInfo.TYPE_SUBTITLE);
+        mCaptionTrackMap.clear();
+        if (captionTracks != null) {
+            for (AtscCaptionTrack captionTrack : captionTracks) {
+                if (mCaptionTrackMap.indexOfKey(captionTrack.serviceNumber) >= 0) {
+                    continue;
+                }
+                String language = captionTrack.language;
+
+                // The service number of the caption service is used for track id of a subtitle.
+                // Later, when a subtitle is chosen, track id will be passed on to TsParser.
+                TvTrackInfo.Builder builder =
+                        new TvTrackInfo.Builder(
+                                TvTrackInfo.TYPE_SUBTITLE,
+                                SUBTITLE_TRACK_PREFIX + captionTrack.serviceNumber);
+                builder.setLanguage(language);
+                mTvTracks.add(builder.build());
+                mCaptionTrackMap.put(captionTrack.serviceNumber, captionTrack);
+            }
+        }
+        mSession.notifyTracksChanged(mTvTracks);
+    }
+
+    private void updateChannelInfo(TunerChannel channel) {
+        if (DEBUG) {
+            Log.d(
+                    TAG,
+                    String.format(
+                            "Channel Info (old) videoPid: %d audioPid: %d " + "audioSize: %d",
+                            mChannel.getVideoPid(),
+                            mChannel.getAudioPid(),
+                            mChannel.getAudioPids().size()));
+        }
+
+        // The list of the audio tracks resided in a channel is often changed depending on a
+        // program being on the air. So, we should update the streaming PIDs and types of the
+        // tuned channel according to the newly received channel data.
+        int oldVideoPid = mChannel.getVideoPid();
+        int oldAudioPid = mChannel.getAudioPid();
+        List<Integer> audioPids = channel.getAudioPids();
+        List<Integer> audioStreamTypes = channel.getAudioStreamTypes();
+        int size = audioPids.size();
+        mChannel.setVideoPid(channel.getVideoPid());
+        mChannel.setAudioPids(audioPids);
+        mChannel.setAudioStreamTypes(audioStreamTypes);
+        updateTvTracks(channel, true);
+        int index = audioPids.isEmpty() ? -1 : 0;
+        for (int i = 0; i < size; ++i) {
+            if (audioPids.get(i) == oldAudioPid) {
+                index = i;
+                break;
+            }
+        }
+        mChannel.selectAudioTrack(index);
+        mSession.notifyTrackSelected(
+                TvTrackInfo.TYPE_AUDIO, index == -1 ? null : AUDIO_TRACK_PREFIX + index);
+
+        // Reset playback if there is a change in the listening streaming PIDs.
+        if (oldVideoPid != mChannel.getVideoPid() || oldAudioPid != mChannel.getAudioPid()) {
+            // TODO: Implement a switching between tracks more smoothly.
+            resetPlayback();
+        }
+        if (DEBUG) {
+            Log.d(
+                    TAG,
+                    String.format(
+                            "Channel Info (new) videoPid: %d audioPid: %d " + " audioSize: %d",
+                            mChannel.getVideoPid(),
+                            mChannel.getAudioPid(),
+                            mChannel.getAudioPids().size()));
+        }
+    }
+
+    private void stopPlayback(boolean removeChannelDataCallbacks) {
+        if (removeChannelDataCallbacks) {
+            mChannelDataManager.removeAllCallbacksAndMessages();
+        }
+        if (mPlayer != null) {
+            mPlayer.setPlayWhenReady(false);
+            mPlayer.release();
+            mPlayer = null;
+            mPlayerState = ExoPlayer.STATE_IDLE;
+            mPlaybackParams.setSpeed(1.0f);
+            mPlayerStarted = false;
+            mReportedDrawnToSurface = false;
+            mPreparingStartTimeMs = INVALID_TIME;
+            mBufferingStartTimeMs = INVALID_TIME;
+            mReadyStartTimeMs = INVALID_TIME;
+            mLastLimitInBytes = 0L;
+            mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_HIDE_AUDIO_UNPLAYABLE);
+            mSession.notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE);
+        }
+    }
+
+    private void startPlayback(int playerHashCode) {
+        // TODO: provide hasAudio()/hasVideo() for play recordings.
+        if (mPlayer == null || System.identityHashCode(mPlayer) != playerHashCode) {
+            return;
+        }
+        if (mChannel != null && !mChannel.hasAudio()) {
+            if (DEBUG) {
+                Log.d(TAG, "Channel " + mChannel + " does not have audio.");
+            }
+            // Playbacks with video-only stream have not been tested yet.
+            // No video-only channel has been found.
+            notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+            return;
+        }
+        if (mChannel != null
+                && ((mChannel.hasAudio() && !mPlayer.hasAudio())
+                        || (mChannel.hasVideo() && !mPlayer.hasVideo()))
+                && mChannel.getType() != Channel.TunerType.TYPE_NETWORK) {
+            // If the channel is from network, skip this part since the video and audio tracks
+            // information for channels from network are more reliable in the extractor. Otherwise,
+            // tracks haven't been detected in the extractor. Try again.
+            sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer));
+            return;
+        }
+        // Since mSurface is volatile, we define a local variable surface to keep the same value
+        // inside this method.
+        Surface surface = mSurface;
+        if (surface != null && !mPlayerStarted) {
+            mPlayer.setSurface(surface);
+            mPlayer.setPlayWhenReady(true);
+            mPlayer.setVolume(mVolume);
+            if (mChannel != null && mPlayer.hasAudio() && !mPlayer.hasVideo()) {
+                notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY);
+            } else if (!mReportedWeakSignal) {
+                // Doesn't show buffering during weak signal.
+                notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING);
+            }
+            mTunerSessionOverlay.sendUiMessage(TunerSessionOverlay.MSG_UI_HIDE_MESSAGE);
+            mPlayerStarted = true;
+        }
+    }
+
+    @VisibleForTesting
+    protected void preparePlayback() {
+        MpegTsPlayer player = createPlayer(mAudioCapabilities);
+        if (!player.prepare(mContext, mChannel, mHasSoftwareAudioDecoder, this)) {
+            mSourceManager.setKeepTuneStatus(false);
+            player.release();
+            if (!mHandler.hasMessages(MSG_TUNE)) {
+                // When prepare failed, there may be some errors related to hardware. In that
+                // case, retry playback immediately may not help.
+                notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
+                Log.i(TAG, "Notify weak signal due to player preparation failure");
+                mHandler.sendMessageDelayed(
+                        mHandler.obtainMessage(
+                                MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)),
+                        PLAYBACK_RETRY_DELAY_MS);
+            }
+        } else {
+            mPlayer = player;
+            mPlayerStarted = false;
+            mHandler.removeMessages(MSG_CHECK_SIGNAL);
+            mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_INITIAL_DELAY_MS);
+            if (mHandler.hasMessages(MSG_CHECK_SIGNAL_STRENGTH)) {
+                mHandler.removeMessages(MSG_CHECK_SIGNAL_STRENGTH);
+            }
+            if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) {
+                mHandler.sendEmptyMessage(MSG_CHECK_SIGNAL_STRENGTH);
+            }
+        }
+    }
+
+    private void resetPlayback() {
+        long timestamp;
+        long oldTimestamp;
+        timestamp = SystemClock.elapsedRealtime();
+        stopPlayback(false);
+        stopCaptionTrack();
+        if (ENABLE_PROFILER) {
+            oldTimestamp = timestamp;
+            timestamp = SystemClock.elapsedRealtime();
+            Log.i(TAG, "[Profiler] stopPlayback() takes " + (timestamp - oldTimestamp) + " ms");
+        }
+        if (mChannelBlocked || mSurface == null || (mChannel == null && mRecordingId == null)) {
+            return;
+        }
+        SoftPreconditions.checkState(mPlayer == null);
+        preparePlayback();
+    }
+
+    private void prepareTune(TunerChannel channel, String recording) {
+        mChannelBlocked = false;
+        mUnblockedContentRating = null;
+        mRetryCount = 0;
+        mChannel = channel;
+        mRecordingId = recording;
+        mRecordingDuration = recording != null ? getDurationForRecording(recording) : null;
+        mProgram = null;
+        mPrograms = null;
+        if (mRecordingId != null) {
+            // Workaround of b/33298048: set it to 1 instead of 0.
+            mBufferStartTimeMs = mRecordStartTimeMs = 1;
+        } else {
+            mBufferStartTimeMs = mRecordStartTimeMs = System.currentTimeMillis();
+        }
+        mLastPositionMs = 0;
+        mCaptionTrack = null;
+        mSignalStrength = TvInputConstantCompat.SIGNAL_STRENGTH_UNKNOWN;
+        if (CommonFeatures.TUNER_SIGNAL_STRENGTH.isEnabled(mContext)) {
+            mSession.notifySignalStrength(mSignalStrength);
+        }
+        mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
+    }
+
+    private void doReschedulePrograms() {
+        long currentPositionMs = getCurrentPosition();
+        long forwardDifference =
+                Math.abs(currentPositionMs - mLastPositionMs - RESCHEDULE_PROGRAMS_INTERVAL_MS);
+        mLastPositionMs = currentPositionMs;
+
+        // A gap is measured as the time difference between previous and next current position
+        // periodically. If the gap has a significant difference with an interval of a period,
+        // this means that there is a change of playback status and the programs of the current
+        // channel should be rescheduled to new playback timeline.
+        if (forwardDifference > RESCHEDULE_PROGRAMS_TOLERANCE_MS) {
+            if (DEBUG) {
+                Log.d(
+                        TAG,
+                        "reschedule programs size:"
+                                + (mPrograms != null ? mPrograms.size() : 0)
+                                + " current program: "
+                                + getCurrentProgram());
+            }
+            mHandler.obtainMessage(MSG_SCHEDULE_OF_PROGRAMS, new Pair<>(mChannel, mPrograms))
+                    .sendToTarget();
+        }
+        mHandler.removeMessages(MSG_RESCHEDULE_PROGRAMS);
+        mHandler.sendEmptyMessageDelayed(MSG_RESCHEDULE_PROGRAMS, RESCHEDULE_PROGRAMS_INTERVAL_MS);
+    }
+
+    private int getTrickPlaySeekIntervalMs() {
+        return Math.max(
+                EXPECTED_KEY_FRAME_INTERVAL_MS / (int) Math.abs(mPlaybackParams.getSpeed()),
+                MIN_TRICKPLAY_SEEK_INTERVAL_MS);
+    }
+
+    @SuppressWarnings("NarrowingCompoundAssignment")
+    private void doTrickplayBySeek(int seekPositionMs) {
+        mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
+        if (mPlaybackParams.getSpeed() == 1.0f || !mPlayer.isPrepared()) {
+            return;
+        }
+        if (seekPositionMs < mBufferStartTimeMs - mRecordStartTimeMs) {
+            if (mPlaybackParams.getSpeed() > 1.0f) {
+                // If fast forwarding, the seekPositionMs can be out of the buffered range
+                // because of chuck evictions.
+                seekPositionMs = (int) (mBufferStartTimeMs - mRecordStartTimeMs);
+            } else {
+                mPlayer.seekTo(mBufferStartTimeMs - mRecordStartTimeMs);
+                mPlaybackParams.setSpeed(1.0f);
+                mPlayer.setAudioTrackAndClosedCaption(true);
+                return;
+            }
+        } else if (seekPositionMs > System.currentTimeMillis() - mRecordStartTimeMs) {
+            // Stops trickplay when FF requested the position later than current position.
+            // If RW trickplay requested the position later than current position,
+            // continue trickplay.
+            if (mPlaybackParams.getSpeed() > 0.0f) {
+                mPlayer.seekTo(System.currentTimeMillis() - mRecordStartTimeMs);
+                mPlaybackParams.setSpeed(1.0f);
+                mPlayer.setAudioTrackAndClosedCaption(true);
+                return;
+            }
+        }
+
+        long delayForNextSeek = getTrickPlaySeekIntervalMs();
+        if (!mPlayer.isBuffering()) {
+            mPlayer.seekTo(seekPositionMs);
+        } else {
+            delayForNextSeek = MIN_TRICKPLAY_SEEK_INTERVAL_MS;
+        }
+        seekPositionMs += mPlaybackParams.getSpeed() * delayForNextSeek;
+        mHandler.sendMessageDelayed(
+                mHandler.obtainMessage(MSG_TRICKPLAY_BY_SEEK, seekPositionMs, 0), delayForNextSeek);
+    }
+
+    private void doTimeShiftPause() {
+        mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR);
+        mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
+        if (!hasEnoughBackwardBuffer()) {
+            return;
+        }
+        mPlaybackParams.setSpeed(1.0f);
+        mPlayer.setPlayWhenReady(false);
+        mPlayer.setAudioTrackAndClosedCaption(true);
+    }
+
+    private void doTimeShiftResume() {
+        mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR);
+        mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
+        mPlaybackParams.setSpeed(1.0f);
+        mPlayer.setPlayWhenReady(true);
+        mPlayer.setAudioTrackAndClosedCaption(true);
+    }
+
+    private void doTimeShiftSeekTo(long timeMs) {
+        mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR);
+        mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
+        mPlayer.seekTo((int) (timeMs - mRecordStartTimeMs));
+    }
+
+    private void doTimeShiftSetPlaybackParams(PlaybackParams params) {
+        if (!hasEnoughBackwardBuffer() && params.getSpeed() < 1.0f) {
+            return;
+        }
+        mPlaybackParams = params;
+        float speed = mPlaybackParams.getSpeed();
+        if (speed == 1.0f) {
+            mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR);
+            mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
+            doTimeShiftResume();
+        } else if (mPlayer.supportSmoothTrickPlay(speed)) {
+            mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
+            mPlayer.setAudioTrackAndClosedCaption(false);
+            mPlayer.startSmoothTrickplay(mPlaybackParams);
+            mHandler.sendEmptyMessageDelayed(
+                    MSG_SMOOTH_TRICKPLAY_MONITOR, TRICKPLAY_MONITOR_INTERVAL_MS);
+        } else {
+            mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR);
+            if (!mHandler.hasMessages(MSG_TRICKPLAY_BY_SEEK)) {
+                mPlayer.setAudioTrackAndClosedCaption(false);
+                mPlayer.setPlayWhenReady(false);
+                // Initiate trickplay
+                mHandler.sendMessage(
+                        mHandler.obtainMessage(
+                                MSG_TRICKPLAY_BY_SEEK,
+                                (int)
+                                        (mPlayer.getCurrentPosition()
+                                                + speed * getTrickPlaySeekIntervalMs()),
+                                0));
+            }
+        }
+    }
+
+    private EitItem getCurrentProgram() {
+        if (mPrograms == null || mPrograms.isEmpty()) {
+            return null;
+        }
+        if (mChannel.getType() == Channel.TunerType.TYPE_FILE) {
+            // For the playback from the local file, we use the first one from the given program.
+            EitItem first = mPrograms.get(0);
+            if (first != null
+                    && (mProgram == null
+                            || first.getStartTimeUtcMillis() < mProgram.getStartTimeUtcMillis())) {
+                return first;
+            }
+            return null;
+        }
+        long currentTimeMs = getCurrentPosition();
+        for (EitItem item : mPrograms) {
+            if (item.getStartTimeUtcMillis() <= currentTimeMs
+                    && item.getEndTimeUtcMillis() >= currentTimeMs) {
+                return item;
+            }
+        }
+        return null;
+    }
+
+    private void doParentalControls() {
+        boolean isParentalControlsEnabled = mTvInputManager.isParentalControlsEnabled();
+        if (isParentalControlsEnabled) {
+            TvContentRating blockContentRating = getContentRatingOfCurrentProgramBlocked();
+            if (DEBUG) {
+                if (blockContentRating != null) {
+                    Log.d(
+                            TAG,
+                            "Check parental controls: blocked by content rating - "
+                                    + blockContentRating);
+                } else {
+                    Log.d(TAG, "Check parental controls: available");
+                }
+            }
+            updateChannelBlockStatus(blockContentRating != null, blockContentRating);
+        } else {
+            if (DEBUG) {
+                Log.d(TAG, "Check parental controls: available");
+            }
+            updateChannelBlockStatus(false, null);
+        }
+    }
+
+    private void doDiscoverCaptionServiceNumber(int serviceNumber) {
+        int index = mCaptionTrackMap.indexOfKey(serviceNumber);
+        if (index < 0) {
+            AtscCaptionTrack captionTrack = new AtscCaptionTrack();
+            captionTrack.serviceNumber = serviceNumber;
+            captionTrack.wideAspectRatio = false;
+            captionTrack.easyReader = false;
+            mCaptionTrackMap.put(serviceNumber, captionTrack);
+            mTvTracks.add(
+                    new TvTrackInfo.Builder(
+                                    TvTrackInfo.TYPE_SUBTITLE,
+                                    SUBTITLE_TRACK_PREFIX + serviceNumber)
+                            .build());
+            mSession.notifyTracksChanged(mTvTracks);
+        }
+    }
+
+    private TvContentRating getContentRatingOfCurrentProgramBlocked() {
+        EitItem currentProgram = getCurrentProgram();
+        if (currentProgram == null) {
+            return null;
+        }
+        ImmutableList<TvContentRating> ratings =
+                mTvContentRatingCache.getRatings(currentProgram.getContentRating());
+        if ((ratings == null || ratings.isEmpty())) {
+            if (Experiments.ENABLE_UNRATED_CONTENT_SETTINGS.get()) {
+                ratings = ImmutableList.of(TvContentRating.UNRATED);
+            } else {
+                ratings = NO_CONTENT_RATINGS;
+            }
+        }
+        for (TvContentRating rating : ratings) {
+            if (!Objects.equals(mUnblockedContentRating, rating)
+                    && mTvInputManager.isRatingBlocked(rating)) {
+                return rating;
+            }
+        }
+        return null;
+    }
+
+    private void updateChannelBlockStatus(boolean channelBlocked, TvContentRating contentRating) {
+        if (mChannelBlocked == channelBlocked) {
+            return;
+        }
+        mChannelBlocked = channelBlocked;
+        if (mChannelBlocked) {
+            clearCallbacksAndMessagesSafely();
+            stopPlayback(true);
+            resetTvTracks();
+            if (contentRating != null) {
+                mSession.notifyContentBlocked(contentRating);
+            }
+            mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS);
+        } else {
+            clearCallbacksAndMessagesSafely();
+            resetPlayback();
+            mSession.notifyContentAllowed();
+            mHandler.sendEmptyMessageDelayed(
+                    MSG_RESCHEDULE_PROGRAMS, RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS);
+            mHandler.removeMessages(MSG_CHECK_SIGNAL);
+            mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_INITIAL_DELAY_MS);
+        }
+    }
+
+    @WorkerThread
+    private void clearCallbacksAndMessagesSafely() {
+        synchronized (mReleaseLock) {
+            if (!mReleaseRequested) {
+                // This check prevents removing MSG_RELEASE from the queue, which would prevent this
+                // session worker from being released.
+                mHandler.removeCallbacksAndMessages(null);
+            }
+        }
+    }
+
+    private boolean hasEnoughBackwardBuffer() {
+        return mPlayer.getCurrentPosition() + BUFFER_UNDERFLOW_BUFFER_MS
+                >= mBufferStartTimeMs - mRecordStartTimeMs;
+    }
+
+    private void notifyVideoUnavailable(final int reason) {
+        mReportedWeakSignal = (reason == TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
+        if (mSession != null) {
+            mSession.notifyVideoUnavailable(reason);
+        }
+    }
+
+    private void notifyVideoAvailable() {
+        mReportedWeakSignal = false;
+        if (mSession != null) {
+            mSession.notifyVideoAvailable();
+        }
+    }
+}
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java b/tuner/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java
index cdcc00d..321c7ba 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java
@@ -156,7 +156,9 @@
                         if (lastModified != 0 && lastModified < now - ELAPSED_MILLIS_TO_DELETE) {
                             // To prevent current recordings from being deleted,
                             // deletes recordings which was not modified for long enough time.
-                            CommonUtils.deleteDirOrFile(recordingDir);
+                            if (!CommonUtils.deleteDirOrFile(recordingDir)) {
+                                Log.w(TAG, "Unable to delete recording data at " + recordingDir);
+                            }
                         }
                     }
                 } catch (IOException | SecurityException e) {
diff --git a/tuner/src/com/android/tv/tuner/tvinput/ChannelDataManager.java b/tuner/src/com/android/tv/tuner/tvinput/datamanager/ChannelDataManager.java
similarity index 95%
rename from tuner/src/com/android/tv/tuner/tvinput/ChannelDataManager.java
rename to tuner/src/com/android/tv/tuner/tvinput/datamanager/ChannelDataManager.java
index c1d8f27..585b28b 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/ChannelDataManager.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/datamanager/ChannelDataManager.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.tv.tuner.tvinput;
+package com.android.tv.tuner.tvinput.datamanager;
 
 import android.content.ContentProviderOperation;
 import android.content.ContentUris;
@@ -32,15 +32,15 @@
 import android.support.annotation.Nullable;
 import android.text.format.DateUtils;
 import android.util.Log;
-import com.android.tv.common.BaseApplication;
+import com.android.tv.common.singletons.HasSingletons;
+import com.android.tv.common.singletons.HasTvInputId;
 import com.android.tv.common.util.PermissionUtils;
-import com.android.tv.tuner.TunerPreferences;
 import com.android.tv.tuner.data.PsipData.EitItem;
 import com.android.tv.tuner.data.TunerChannel;
+import com.android.tv.tuner.prefs.TunerPreferences;
 import com.android.tv.tuner.util.ConvertUtils;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -100,7 +100,7 @@
     private final Context mContext;
     private final String mInputId;
     private ProgramInfoListener mListener;
-    private ChannelScanListener mChannelScanListener;
+    private ChannelHandlingDoneListener mChannelHandlingDoneListener;
     private Handler mChannelScanHandler;
     private final HandlerThread mHandlerThread;
     private final Handler mHandler;
@@ -140,14 +140,15 @@
         void onRescanNeeded();
     }
 
-    public interface ChannelScanListener {
+    /** Listens for all channel handling to be done. */
+    public interface ChannelHandlingDoneListener {
         /** Invoked when all pending channels have been handled. */
         void onChannelHandlingDone();
     }
 
     public ChannelDataManager(Context context) {
         mContext = context;
-        mInputId = BaseApplication.getSingletons(context).getEmbeddedTunerInputId();
+        mInputId = HasSingletons.get(HasTvInputId.class, context).getEmbeddedTunerInputId();
         mChannelsUri = TvContract.buildChannelsUriForInput(mInputId);
         mTunerChannelMap = new ConcurrentHashMap<>();
         mTunerChannelIdMap = new ConcurrentSkipListMap<>();
@@ -185,8 +186,8 @@
         mListener = listener;
     }
 
-    public void setChannelScanListener(ChannelScanListener listener, Handler handler) {
-        mChannelScanListener = listener;
+    public void setChannelScanListener(ChannelHandlingDoneListener listener, Handler handler) {
+        mChannelHandlingDoneListener = listener;
         mChannelScanHandler = handler;
     }
 
@@ -198,7 +199,7 @@
     public void releaseSafely() {
         mHandlerThread.quitSafely();
         mListener = null;
-        mChannelScanListener = null;
+        mChannelHandlingDoneListener = null;
         mChannelScanHandler = null;
     }
 
@@ -305,16 +306,10 @@
                 Log.e(TAG, "Error deleting obsolete channels", e);
             }
         }
-        if (mChannelScanListener != null && mChannelScanHandler != null) {
-            mChannelScanHandler.post(
-                    new Runnable() {
-                        @Override
-                        public void run() {
-                            mChannelScanListener.onChannelHandlingDone();
-                        }
-                    });
+        if (mChannelHandlingDoneListener != null && mChannelScanHandler != null) {
+            mChannelScanHandler.post(() -> mChannelHandlingDoneListener.onChannelHandlingDone());
         } else {
-            Log.e(TAG, "Error. mChannelScanListener is null.");
+            Log.e(TAG, "Error. mChannelHandlingDoneListener is null.");
         }
     }
 
@@ -441,14 +436,10 @@
                             Collections.binarySearch(
                                     oldItems,
                                     newItem,
-                                    new Comparator<EitItem>() {
-                                        @Override
-                                        public int compare(EitItem lhs, EitItem rhs) {
-                                            return Long.compare(
+                                    (EitItem lhs, EitItem rhs) ->
+                                            Long.compare(
                                                     lhs.getStartTimeUtcMillis(),
-                                                    rhs.getStartTimeUtcMillis());
-                                        }
-                                    });
+                                                    rhs.getStartTimeUtcMillis()));
                     if (pos >= 0) {
                         // Same start Time found. Overlapped.
                         continue;
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerDebug.java b/tuner/src/com/android/tv/tuner/tvinput/debug/TunerDebug.java
similarity index 98%
rename from tuner/src/com/android/tv/tuner/tvinput/TunerDebug.java
rename to tuner/src/com/android/tv/tuner/tvinput/debug/TunerDebug.java
index 1df0b5c..a92bc59 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/TunerDebug.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/debug/TunerDebug.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.tv.tuner.tvinput;
+package com.android.tv.tuner.tvinput.debug;
 
 import android.os.SystemClock;
 import android.util.Log;
diff --git a/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactory.java b/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactory.java
new file mode 100644
index 0000000..a27cb22
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactory.java
@@ -0,0 +1,25 @@
+package com.android.tv.tuner.tvinput.factory;
+
+import android.content.Context;
+import android.media.tv.TvInputService.Session;
+import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
+
+/** {@link android.media.tv.TvInputService.Session} factory */
+public interface TunerSessionFactory {
+
+    /** Called when a session is released */
+    interface SessionReleasedCallback {
+
+        /**
+         * Called when the given session is released.
+         *
+         * @param session The session that has been released.
+         */
+        void onReleased(Session session);
+    }
+
+    Session create(
+            Context context,
+            ChannelDataManager channelDataManager,
+            SessionReleasedCallback releasedCallback);
+}
diff --git a/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactoryImpl.java b/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactoryImpl.java
new file mode 100644
index 0000000..54e959e
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactoryImpl.java
@@ -0,0 +1,49 @@
+package com.android.tv.tuner.tvinput.factory;
+
+import android.content.Context;
+import android.media.tv.TvInputService.Session;
+import com.android.tv.tuner.source.TsDataSourceManager;
+import com.android.tv.tuner.tvinput.TunerSession;
+import com.android.tv.tuner.tvinput.TunerSessionExoV2;
+import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
+import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+import com.android.tv.common.flags.TunerFlags;
+import javax.inject.Inject;
+
+/** Creates a {@link TunerSessionFactory}. */
+public class TunerSessionFactoryImpl implements TunerSessionFactory {
+
+    private final TunerFlags mTunerFlags;
+    private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
+    private final TsDataSourceManager.Factory mTsDataSourceManagerFactory;
+
+    @Inject
+    public TunerSessionFactoryImpl(
+            TunerFlags tunerFlags,
+            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
+            TsDataSourceManager.Factory tsDataSourceManagerFactory) {
+        mTunerFlags = tunerFlags;
+        mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
+        mTsDataSourceManagerFactory = tsDataSourceManagerFactory;
+    }
+
+    @Override
+    public Session create(
+            Context context,
+            ChannelDataManager channelDataManager,
+            SessionReleasedCallback releasedCallback) {
+        return mTunerFlags.useExoplayerV2()
+                ? new TunerSessionExoV2(
+                        context,
+                        channelDataManager,
+                        releasedCallback,
+                        mConcurrentDvrPlaybackFlags,
+                        mTsDataSourceManagerFactory)
+                : new TunerSession(
+                        context,
+                        channelDataManager,
+                        releasedCallback,
+                        mConcurrentDvrPlaybackFlags,
+                        mTsDataSourceManagerFactory);
+    }
+}
diff --git a/tuner/src/com/android/tv/tuner/util/TunerInputInfoUtils.java b/tuner/src/com/android/tv/tuner/util/TunerInputInfoUtils.java
deleted file mode 100644
index fad7133..0000000
--- a/tuner/src/com/android/tv/tuner/util/TunerInputInfoUtils.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tv.tuner.util;
-
-import android.annotation.TargetApi;
-import android.content.ComponentName;
-import android.content.Context;
-import android.media.tv.TvInputInfo;
-import android.media.tv.TvInputManager;
-import android.os.AsyncTask;
-import android.os.Build;
-import android.support.annotation.Nullable;
-import android.util.Log;
-import android.util.Pair;
-import com.android.tv.common.BaseApplication;
-import com.android.tv.common.BuildConfig;
-import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.tuner.R;
-import com.android.tv.tuner.TunerHal;
-
-/** Utility class for providing tuner input info. */
-public class TunerInputInfoUtils {
-    private static final String TAG = "TunerInputInfoUtils";
-    private static final boolean DEBUG = false;
-
-    /** Builds tuner input's info. */
-    @Nullable
-    @TargetApi(Build.VERSION_CODES.N)
-    public static TvInputInfo buildTunerInputInfo(Context context) {
-        Pair<Integer, Integer> tunerTypeAndCount = TunerHal.getTunerTypeAndCount(context);
-        if (tunerTypeAndCount.first == null || tunerTypeAndCount.second == 0) {
-            return null;
-        }
-        int inputLabelId = 0;
-        switch (tunerTypeAndCount.first) {
-            case TunerHal.TUNER_TYPE_BUILT_IN:
-                inputLabelId = R.string.bt_app_name;
-                break;
-            case TunerHal.TUNER_TYPE_USB:
-                inputLabelId = R.string.ut_app_name;
-                break;
-            case TunerHal.TUNER_TYPE_NETWORK:
-                inputLabelId = R.string.nt_app_name;
-                break;
-        }
-        try {
-            String inputId = BaseApplication.getSingletons(context).getEmbeddedTunerInputId();
-            TvInputInfo.Builder builder =
-                    new TvInputInfo.Builder(context, ComponentName.unflattenFromString(inputId));
-            return builder.setLabel(inputLabelId)
-                    .setCanRecord(CommonFeatures.DVR.isEnabled(context))
-                    .setTunerCount(tunerTypeAndCount.second)
-                    .build();
-        } catch (IllegalArgumentException | NullPointerException e) {
-            // BaseTunerTvInputService is not enabled.
-            return null;
-        }
-    }
-
-    /**
-     * Updates tuner input's info.
-     *
-     * @param context {@link Context} instance
-     */
-    public static void updateTunerInputInfo(Context context) {
-        final Context appContext = context.getApplicationContext();
-        if (!BuildConfig.NO_JNI_TEST && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-            new AsyncTask<Void, Void, TvInputInfo>() {
-                @Override
-                protected TvInputInfo doInBackground(Void... params) {
-                    if (DEBUG) Log.d(TAG, "updateTunerInputInfo()");
-                    return buildTunerInputInfo(appContext);
-                }
-
-                @Override
-                @TargetApi(Build.VERSION_CODES.N)
-                protected void onPostExecute(TvInputInfo info) {
-                    if (info != null) {
-                        ((TvInputManager) appContext.getSystemService(Context.TV_INPUT_SERVICE))
-                                .updateTvInputInfo(info);
-                        if (DEBUG) {
-                            Log.d(
-                                    TAG,
-                                    "TvInputInfo ["
-                                            + info.loadLabel(appContext)
-                                            + "] updated: "
-                                            + info.toString());
-                        }
-                    } else {
-                        if (DEBUG) {
-                            Log.d(TAG, "Updating tuner input info failed. Input is not ready yet.");
-                        }
-                    }
-                }
-            }.execute();
-        }
-    }
-}
diff --git a/tuner/tests/testing/Android.mk b/tuner/tests/testing/Android.mk
index c0d5dda..79e35e5 100644
--- a/tuner/tests/testing/Android.mk
+++ b/tuner/tests/testing/Android.mk
@@ -8,11 +8,10 @@
 
 LOCAL_STATIC_JAVA_LIBRARIES := \
     android-support-annotations \
-    android-support-test \
-    guava \
+    androidx.test.runner \
+    tv-guava-android-jar \
     mockito-target \
-    platform-robolectric-3.6.2-prebuilt \
-    truth-0-36-prebuilt-jar \
+    tv-lib-truth \
     ub-uiautomator \
 
 # Link tv-common as shared library to avoid the problem of initialization of the constants
diff --git a/tuner/tests/testing/AndroidManifest.xml b/tuner/tests/testing/AndroidManifest.xml
index f244ae7..7e07a52 100644
--- a/tuner/tests/testing/AndroidManifest.xml
+++ b/tuner/tests/testing/AndroidManifest.xml
@@ -18,6 +18,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.tv.tuner.testing"
           android:versionCode="1">
-  <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="21"/>
+  <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
     <application />
 </manifest>
diff --git a/tuner/tests/unittests/javatests/AndroidManifest.xml b/tuner/tests/unittests/javatests/AndroidManifest.xml
index 8a5fda8..62caefa 100644
--- a/tuner/tests/unittests/javatests/AndroidManifest.xml
+++ b/tuner/tests/unittests/javatests/AndroidManifest.xml
@@ -18,7 +18,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.tv.tuner.layout.tests" >
 
-    <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="26"/>
+    <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="27"/>
 
     <instrumentation
         android:name="android.test.InstrumentationTestRunner"
diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/AndroidManifest.xml b/tuner/tests/unittests/javatests/com/android/tv/tuner/AndroidManifest.xml
index 9c81560..6fe0b85 100644
--- a/tuner/tests/unittests/javatests/com/android/tv/tuner/AndroidManifest.xml
+++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/AndroidManifest.xml
@@ -18,10 +18,10 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.tv.tuner.tests" >
 
-    <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="26" />
+    <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="27" />
 
     <instrumentation
-        android:name="android.support.test.runner.AndroidJUnitRunner"
+        android:name="androidx.test.runner.AndroidJUnitRunner"
         android:targetPackage="com.android.tv" />
 
     <application android:label="TunerTvInputTests" >
diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/FakeTunerHal.java b/tuner/tests/unittests/javatests/com/android/tv/tuner/FakeTunerHal.java
index cc4f6fd..6d113b0 100644
--- a/tuner/tests/unittests/javatests/com/android/tv/tuner/FakeTunerHal.java
+++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/FakeTunerHal.java
@@ -25,19 +25,19 @@
     }
 
     @Override
-    protected boolean openFirstAvailable() {
+    public boolean openFirstAvailable() {
         mDeviceOpened = true;
         getDeliverySystemTypeFromDevice();
         return true;
     }
 
     @Override
-    protected boolean isDeviceOpen() {
+    public boolean isDeviceOpen() {
         return mDeviceOpened;
     }
 
     @Override
-    protected long getDeviceId() {
+    public long getDeviceId() {
         return 0;
     }
 
diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/FileTunerHal.java b/tuner/tests/unittests/javatests/com/android/tv/tuner/FileTunerHal.java
index 73d234e..cb46483 100644
--- a/tuner/tests/unittests/javatests/com/android/tv/tuner/FileTunerHal.java
+++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/FileTunerHal.java
@@ -76,7 +76,7 @@
     }
 
     @Override
-    protected boolean openFirstAvailable() {
+    public boolean openFirstAvailable() {
         sIsDeviceOpen = true;
         getDeliverySystemTypeFromDevice();
         return true;
@@ -86,12 +86,12 @@
     public void close() {}
 
     @Override
-    protected boolean isDeviceOpen() {
+    public boolean isDeviceOpen() {
         return sIsDeviceOpen;
     }
 
     @Override
-    protected long getDeviceId() {
+    public long getDeviceId() {
         return DEVICE_ID;
     }
 
diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/ZappingTimeTest.java b/tuner/tests/unittests/javatests/com/android/tv/tuner/ZappingTimeTest.java
index 0e9bd35..ef653f8 100644
--- a/tuner/tests/unittests/javatests/com/android/tv/tuner/ZappingTimeTest.java
+++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/ZappingTimeTest.java
@@ -21,10 +21,11 @@
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Message;
-import android.support.test.filters.LargeTest;
 import android.test.InstrumentationTestCase;
 import android.util.Log;
 import android.view.Surface;
+import androidx.test.filters.LargeTest;
+import com.android.tv.common.flags.impl.DefaultConcurrentDvrPlaybackFlags;
 import com.android.tv.tuner.data.Cea708Data;
 import com.android.tv.tuner.data.PsiData;
 import com.android.tv.tuner.data.PsipData;
@@ -33,10 +34,10 @@
 import com.android.tv.tuner.exoplayer.MpegTsPlayer;
 import com.android.tv.tuner.exoplayer.MpegTsRendererBuilder;
 import com.android.tv.tuner.exoplayer.buffer.BufferManager;
+import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener;
 import com.android.tv.tuner.exoplayer.buffer.TrickplayStorageManager;
 import com.android.tv.tuner.source.TsDataSourceManager;
-import com.android.tv.tuner.tvinput.EventDetector;
-import com.android.tv.tuner.tvinput.PlaybackBufferListener;
+import com.android.tv.tuner.ts.EventDetector.EventListener;
 import com.google.android.exoplayer.ExoPlayer;
 import java.io.File;
 import java.io.FileOutputStream;
@@ -86,7 +87,9 @@
     private AtomicLong mOnDrawnToSurfaceTimeMs = new AtomicLong(0);
     private MockMpegTsPlayerListener mMpegTsPlayerListener = new MockMpegTsPlayerListener();
     private MockPlaybackBufferListener mPlaybackBufferListener = new MockPlaybackBufferListener();
-    private MockEventListener mEventListener = new MockEventListener();
+    private MockChannelScanListener mEventListener = new MockChannelScanListener();
+    private DefaultConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags =
+            new DefaultConcurrentDvrPlaybackFlags();
 
     @Override
     protected void setUp() throws Exception {
@@ -152,7 +155,8 @@
                                                             new MpegTsRendererBuilder(
                                                                     mTargetContext,
                                                                     bufferManager,
-                                                                    mPlaybackBufferListener),
+                                                                    mPlaybackBufferListener,
+                                                                    mConcurrentDvrPlaybackFlags),
                                                             mHandler,
                                                             mSourceManager,
                                                             null,
@@ -388,7 +392,7 @@
         }
     }
 
-    private static class MockEventListener implements EventDetector.EventListener {
+    private static class MockChannelScanListener implements EventListener {
         @Override
         public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) {
             if (DEBUG) {
diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/AndroidManifest.xml b/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/AndroidManifest.xml
index 3e6946a..77c7f40 100644
--- a/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/AndroidManifest.xml
+++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/AndroidManifest.xml
@@ -17,7 +17,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.tv.tuner"
     android:versionCode="1">
-  <uses-sdk android:targetSdkVersion="26" android:minSdkVersion="23"/>
+  <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
   <application android:label="TunerTvInputLayoutTests" >
     <activity android:name="com.android.tv.tuner.layout.tests.ScaledLayoutActivity"
               android:label="ScaledLayout Test" />
diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/ScaledLayoutTest.java b/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/ScaledLayoutTest.java
index 214b063..c2a23f2 100644
--- a/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/ScaledLayoutTest.java
+++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/ScaledLayoutTest.java
@@ -20,11 +20,11 @@
 import static junit.framework.Assert.assertNotNull;
 
 import android.content.Intent;
-import android.support.test.filters.SmallTest;
-import android.support.test.rule.ActivityTestRule;
-import android.support.test.runner.AndroidJUnit4;
 import android.view.View;
 import android.widget.FrameLayout;
+import androidx.test.filters.SmallTest;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
 import com.android.tv.tuner.layout.ScaledLayout;
 import org.junit.After;
 import org.junit.Before;
diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/setup/TunerHalFactoryTest.java b/tuner/tests/unittests/javatests/com/android/tv/tuner/setup/TunerHalCreatorTest.java
similarity index 60%
rename from tuner/tests/unittests/javatests/com/android/tv/tuner/setup/TunerHalFactoryTest.java
rename to tuner/tests/unittests/javatests/com/android/tv/tuner/setup/TunerHalCreatorTest.java
index 2354c82..a3a3208 100644
--- a/tuner/tests/unittests/javatests/com/android/tv/tuner/setup/TunerHalFactoryTest.java
+++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/setup/TunerHalCreatorTest.java
@@ -21,27 +21,27 @@
 import static org.junit.Assert.assertSame;
 
 import android.os.AsyncTask;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-import com.android.tv.tuner.TunerHal;
-import com.android.tv.tuner.setup.BaseTunerSetupActivity.TunerHalFactory;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+import com.android.tv.tuner.api.Tuner;
+import com.android.tv.tuner.setup.BaseTunerSetupActivity.TunerHalCreator;
 import java.util.concurrent.Executor;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-/** Tests for {@link TunerHalFactory}. */
+/** Tests for {@link TunerHalCreator}. */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class TunerHalFactoryTest {
+public class TunerHalCreatorTest {
     private final FakeExecutor mFakeExecutor = new FakeExecutor();
 
-    private static class TestTunerHalFactory extends TunerHalFactory {
-        private TestTunerHalFactory(Executor executor) {
+    private static class TestTunerHalCreator extends TunerHalCreator {
+        private TestTunerHalCreator(Executor executor) {
             super(null, executor);
         }
 
         @Override
-        protected TunerHal createInstance() {
+        protected Tuner createInstance() {
             return new com.android.tv.tuner.FakeTunerHal() {};
         }
     }
@@ -61,29 +61,29 @@
 
     @Test
     public void test_asyncGet() {
-        TunerHalFactory tunerHalFactory = new TestTunerHalFactory(mFakeExecutor);
-        assertNull(tunerHalFactory.mTunerHal);
-        tunerHalFactory.generate();
-        assertNull(tunerHalFactory.mTunerHal);
+        TunerHalCreator tunerHalCreator = new TestTunerHalCreator(mFakeExecutor);
+        assertNull(tunerHalCreator.mTunerHal);
+        tunerHalCreator.generate();
+        assertNull(tunerHalCreator.mTunerHal);
         mFakeExecutor.executeActually();
-        TunerHal tunerHal = tunerHalFactory.getOrCreate();
+        Tuner tunerHal = tunerHalCreator.getOrCreate();
         assertNotNull(tunerHal);
-        assertSame(tunerHal, tunerHalFactory.getOrCreate());
-        tunerHalFactory.clear();
+        assertSame(tunerHal, tunerHalCreator.getOrCreate());
+        tunerHalCreator.clear();
     }
 
     @Test
     public void test_syncGet() {
-        TunerHalFactory tunerHalFactory = new TestTunerHalFactory(AsyncTask.SERIAL_EXECUTOR);
-        assertNull(tunerHalFactory.mTunerHal);
-        tunerHalFactory.generate();
-        assertNotNull(tunerHalFactory.getOrCreate());
+        TunerHalCreator tunerHalCreator = new TestTunerHalCreator(AsyncTask.SERIAL_EXECUTOR);
+        assertNull(tunerHalCreator.mTunerHal);
+        tunerHalCreator.generate();
+        assertNotNull(tunerHalCreator.getOrCreate());
     }
 
     @Test
     public void test_syncGetWithoutGenerate() {
-        TunerHalFactory tunerHalFactory = new TestTunerHalFactory(mFakeExecutor);
-        assertNull(tunerHalFactory.mTunerHal);
-        assertNotNull(tunerHalFactory.getOrCreate());
+        TunerHalCreator tunerHalCreator = new TestTunerHalCreator(mFakeExecutor);
+        assertNull(tunerHalCreator.mTunerHal);
+        assertNotNull(tunerHalCreator.getOrCreate());
     }
 }