[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;
-