[automerger skipped] Merge qt-r1-dev-plus-aosp-without-vendor (5817612) into stage-aosp-master am: c09c875c6a -s ours am: 2d13e2124b -s ours
am: cbfb9593be -s ours
am skip reason: change_id I473cbf8f4f53a00f4c7dafce18a5ad23a1614332 with SHA1 b0c5908615 is in history

Change-Id: I9d725eb2daaba5205a13943df4c75db11b31a3c7
diff --git a/Android.bp b/Android.bp
index 4268636..83c232b 100644
--- a/Android.bp
+++ b/Android.bp
@@ -14,8 +14,8 @@
 // limitations under the License.
 //
 
-version_name = "1.20-asop"
-version_code = "417000328"
+version_name = "1.23-asop"
+version_code = "417000410"
 
 android_app {
     name: "LiveTv",
@@ -44,16 +44,13 @@
     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",
+        "androidx.legacy_legacy-support-core-ui",
+        "androidx.leanback_leanback",
+        "androidx.leanback_leanback-preference",
+        "androidx.palette_palette",
+        "androidx.preference_preference",
+        "androidx.tvprovider_tvprovider",
         "jsr330",
         "live-channels-partner-support",
         "live-tv-tuner-proto",
@@ -62,6 +59,7 @@
         "tv-auto-factory-jar",
         "tv-common",
         "tv-error-prone-annotations-jar",
+        "tv-javax-annotations-jar",
         "tv-lib-dagger",
         "tv-lib-exoplayer",
         "tv-lib-exoplayer-v2-core",
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index a398823..7110160 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -16,12 +16,12 @@
 -->
 <!-- This manifest is for LiveTv -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-  xmlns:tools="http://schemas.android.com/tools"
+    xmlns:tools="http://schemas.android.com/tools"
     package="com.android.tv" >
 
     <uses-sdk
         android:minSdkVersion="23"
-        android:targetSdkVersion="27" />
+        android:targetSdkVersion="28" />
 
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@@ -79,8 +79,7 @@
         android:label="@string/app_name"
         android:supportsRtl="true"
         android:theme="@style/Theme.TV"
-        tools:replace="android:appComponentFactory">
-        >
+        tools:replace="android:appComponentFactory" >
 
         <!-- providers are listed here to keep them separate from the internal versions -->
         <provider
@@ -254,12 +253,16 @@
             android:name="com.android.tv.recommendation.ChannelPreviewUpdater$ChannelPreviewUpdateService"
             android:permission="android.permission.BIND_JOB_SERVICE" />
 
-        <receiver android:name="com.android.tv.receiver.BootCompletedReceiver" >
+        <receiver
+            android:name="com.android.tv.receiver.BootCompletedReceiver"
+            android:exported="true" >
             <intent-filter>
                 <action android:name="android.intent.action.BOOT_COMPLETED" />
             </intent-filter>
         </receiver>
-        <receiver android:name="com.android.tv.receiver.PackageIntentsReceiver" >
+        <receiver
+            android:name="com.android.tv.receiver.PackageIntentsReceiver"
+            android:exported="true" >
             <intent-filter>
                 <action android:name="android.intent.action.PACKAGE_ADDED" />
                 <!-- PACKAGE_CHANGED for package enabled/disabled notification -->
@@ -290,11 +293,13 @@
             android:name="com.android.tv.dvr.recorder.DvrRecordingService"
             android:label="@string/dvr_service_name" />
 
-        <receiver android:name="com.android.tv.dvr.recorder.DvrStartRecordingReceiver" />
+        <receiver
+            android:name="com.android.tv.dvr.recorder.DvrStartRecordingReceiver"
+            android:exported="false" />
 
         <service
             android:name="com.android.tv.data.epg.EpgFetchService"
             android:permission="android.permission.BIND_JOB_SERVICE" />
     </application>
 
-</manifest>
+</manifest>
\ No newline at end of file
diff --git a/README.md b/README.md
index 63c1f44..0659bbd 100644
--- a/README.md
+++ b/README.md
@@ -2,18 +2,6 @@
 
 __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
diff --git a/assets/rating_sources.html b/assets/rating_sources.html
index 50da7cc..ff4a005 100644
--- a/assets/rating_sources.html
+++ b/assets/rating_sources.html
@@ -89,4 +89,22 @@
 <pre>
     Source: http://www.mpaa.org/film-ratings/
 </pre>
+<ul>
+    <li>TV content rating system strings for DTMB</li>
+</ul>
+<pre>
+    Source: http://www.gb688.cn/bzgk/gb/newGbInfo?hcno=59E83CA701AEB4248E115BC043688FEC
+</pre>
+<ul>
+    <li>Implementations details of TV content rating system strings for New Zealand</li>
+</ul>
+<pre>
+    Source: https://bsa.govt.nz/images/03_BSA_FREE-TO-AIR-TV_CLASSIFICATIONS_DRAFT.pdf
+</pre>
+<ul>
+    <li>TV content rating system strings for Thailand</li>
+</ul>
+<pre>
+    Source: https://broadcast.nbtc.go.th/law/dwl.php?id=NjAwODAwMDAwMDAx&file=ZGF0YS9kb2N1bWVudC9sYXcvZG9jL3RoLzYwMDgwMDAwMDAwMS5wZGY=
+</pre>
 </body></html>
diff --git a/build.gradle b/build.gradle
index 23e3dbd..10cddcc 100644
--- a/build.gradle
+++ b/build.gradle
@@ -18,37 +18,28 @@
 /*
  * Experimental gradle configuration.  This file may not be up to date.
  */
+apply plugin: 'com.android.application'
 
 buildscript {
     repositories {
-        mavenCentral()
         google()
+        jcenter()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.1.4'
-        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.6'
+        classpath 'com.android.tools.build:gradle:3.4.2'
+        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
     }
 }
-apply plugin: 'com.android.application'
+
 android {
     compileSdkVersion 28
     buildToolsVersion '28.0.3'
-    dexOptions {
-        preDexLibraries = false
-        additionalParameters=['--core-library']
-        javaMaxHeapSize "6g"
+
+    compileOptions() {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
     }
-    android {
-        defaultConfig {
-            resConfigs "en"
-        }
-    }
-    defaultConfig {
-        minSdkVersion 23
-        targetSdkVersion 28
-        versionCode 1
-        versionName "1.0"
-    }
+
     buildTypes {
         debug {
             minifyEnabled false
@@ -57,10 +48,15 @@
             minifyEnabled true
         }
     }
-    compileOptions() {
-        sourceCompatibility JavaVersion.VERSION_1_8
-        targetCompatibility JavaVersion.VERSION_1_8
+
+    defaultConfig {
+        minSdkVersion 23
+        resConfigs "en"
+        targetSdkVersion 28
+        versionCode 1
+        versionName "1.0"
     }
+
     sourceSets {
         main {
             res.srcDirs = ['res', 'material_res']
@@ -70,30 +66,33 @@
     }
 }
 
-repositories {
-    mavenCentral()
-    jcenter()
-    google()
+allprojects {
+    repositories {
+        google()
+        jcenter()
+    }
 }
 
 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      'androidx.appcompat:appcompat:1.0.2'
+    implementation      'androidx.core:core:1.0.2'
+    implementation      'androidx.palette:palette:1.0.0'
+    implementation      'androidx.leanback:leanback:1.1.0-alpha02'
+    implementation      'androidx.recyclerview:recyclerview:1.0.0'
+    implementation      'androidx.recyclerview:recyclerview-selection:1.0.0'
+    implementation      'androidx.tvprovider:tvprovider: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'
+    annotationProcessor 'com.google.auto.factory:auto-factory:1.0-beta6'
+    implementation      'com.google.auto.factory:auto-factory:1.0-beta6'
+    annotationProcessor 'com.google.auto.value:auto-value:1.5.3'
+    implementation      'com.google.auto.value:auto-value:1.5.3'
+    implementation      'com.google.dagger:dagger:2.23'
+    implementation      'com.google.dagger:dagger-android:2.23'
+    annotationProcessor 'com.google.dagger:dagger-android-processor:2.23'
+    annotationProcessor 'com.google.dagger:dagger-compiler:2.23'
+    implementation      'com.google.guava:guava:28.0-jre'
 
-    /*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
+    implementation      'javax.inject:javax.inject:1'
+
+    implementation      project(':common')
+}
diff --git a/common/Android.bp b/common/Android.bp
index 63759d4..bb709cf 100644
--- a/common/Android.bp
+++ b/common/Android.bp
@@ -28,22 +28,27 @@
     resource_dirs: ["res"],
 
     libs: [
+        "android-support-annotations",
         "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",
+        "tv-javax-annotations-jar",
+
     ],
 
-    static_libs: ["tv-lib-dagger-android"],
+    static_libs: [
+            "androidx.legacy_legacy-support-core-ui",
+            "androidx.appcompat_appcompat",
+            "androidx.preference_preference",
+            "androidx.leanback_leanback",
+            "androidx.tvprovider_tvprovider",
+            "tv-guava-android-jar",
+            "jsr330",
+            "tv-lib-dagger",
+            "tv-lib-exoplayer",
+            "tv-lib-exoplayer-v2-core",
+    "tv-lib-dagger-android",
+    ],
 
     plugins: [
         "tv-auto-value",
diff --git a/common/AndroidManifest.xml b/common/AndroidManifest.xml
index 7002d5f..eb7de57 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="27" android:minSdkVersion="23"/>
+    <uses-sdk android:targetSdkVersion="28" android:minSdkVersion="23"/>
     <application />
 </manifest>
diff --git a/common/build.gradle b/common/build.gradle
index f371475..b7bc886 100644
--- a/common/build.gradle
+++ b/common/build.gradle
@@ -21,37 +21,24 @@
 
 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"
-        }
+    compileOptions() {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
     }
 
     defaultConfig {
         minSdkVersion 23
+        resConfigs "en"
         targetSdkVersion 28
         versionCode 1
         versionName "1.0"
     }
+
     buildTypes {
         debug {
             minifyEnabled false
@@ -66,10 +53,6 @@
             buildConfigField "boolean", "NO_JNI_TEST", "false"
         }
     }
-    compileOptions() {
-        sourceCompatibility JavaVersion.VERSION_1_8
-        targetCompatibility JavaVersion.VERSION_1_8
-    }
 
     sourceSets {
         main {
@@ -79,29 +62,32 @@
             proto {
                 srcDir 'src/com/android/tv/common/compat/internal'
             }
+            proto {
+                srcDir 'src/com/android/tv/common/flags/proto'
+            }
         }
     }
 }
 
-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'
+    implementation      'androidx.annotation:annotation:1.1.0'
+    implementation      'androidx.appcompat:appcompat:1.0.2'
+    implementation      'androidx.leanback:leanback:1.1.0-alpha02'
+    implementation      'androidx.palette:palette:1.0.0'
+    implementation      'androidx.recyclerview:recyclerview:1.0.0'
+    implementation      'androidx.recyclerview:recyclerview-selection:1.0.0'
+    implementation      'androidx.tvprovider:tvprovider:1.0.0'
+
+    implementation      'com.google.android.exoplayer:exoplayer:r1.5.16'
+    implementation      'com.google.android.exoplayer:exoplayer-core:2.10.1'
+    annotationProcessor 'com.google.auto.value:auto-value:1.5.3'
+    implementation      'com.google.auto.value:auto-value:1.5.3'
+    implementation      'com.google.dagger:dagger:2.23'
+    implementation      'com.google.dagger:dagger-android:2.23'
+    annotationProcessor 'com.google.dagger:dagger-android-processor:2.23'
+    annotationProcessor 'com.google.dagger:dagger-compiler:2.23'
+    implementation      'com.google.guava:guava:28.0-jre'
+    implementation      'com.google.protobuf:protobuf-java:3.0.0'
 }
 protobuf {
     // Configure the protoc executable
diff --git a/common/src/com/android/tv/common/BaseApplication.java b/common/src/com/android/tv/common/BaseApplication.java
index 45c3256..1a42120 100644
--- a/common/src/com/android/tv/common/BaseApplication.java
+++ b/common/src/com/android/tv/common/BaseApplication.java
@@ -21,17 +21,22 @@
 import android.os.Build;
 import android.os.StrictMode;
 import android.support.annotation.VisibleForTesting;
+
+import com.android.tv.common.dev.DeveloperPreferences;
 import com.android.tv.common.feature.CommonFeatures;
 import com.android.tv.common.recording.RecordingStorageStatusManager;
 import com.android.tv.common.util.Clock;
 import com.android.tv.common.util.CommonUtils;
 import com.android.tv.common.util.Debug;
-import com.android.tv.common.util.SystemProperties;
+
+import dagger.Lazy;
 import dagger.android.DaggerApplication;
 
-/** The base application class for Live TV applications. */
+import javax.inject.Inject;
+
+/** The base application class for TV applications. */
 public abstract class BaseApplication extends DaggerApplication implements BaseSingletons {
-    private RecordingStorageStatusManager mRecordingStorageStatusManager;
+    @Inject Lazy<RecordingStorageStatusManager> mRecordingStorageStatusManager;
 
     /**
      * An instance of {@link BaseSingletons}. Note that this can be set directly only for the test
@@ -65,7 +70,7 @@
 
         // Only set StrictMode for ENG builds because the build server only produces userdebug
         // builds.
-        if (BuildConfig.ENG && SystemProperties.ALLOW_STRICT_MODE.getValue()) {
+        if (BuildConfig.ENG && DeveloperPreferences.ALLOW_STRICT_MODE.get(this)) {
             StrictMode.ThreadPolicy.Builder threadPolicyBuilder =
                     new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog();
             // TODO(b/69565157): Turn penaltyDeath on for VMPolicy when tests are fixed.
@@ -99,9 +104,6 @@
     @Override
     @TargetApi(Build.VERSION_CODES.N)
     public RecordingStorageStatusManager getRecordingStorageStatusManager() {
-        if (mRecordingStorageStatusManager == null) {
-            mRecordingStorageStatusManager = new RecordingStorageStatusManager(this);
-        }
-        return mRecordingStorageStatusManager;
+        return mRecordingStorageStatusManager.get();
     }
 }
diff --git a/common/src/com/android/tv/common/BaseSingletons.java b/common/src/com/android/tv/common/BaseSingletons.java
index 1053061..8a3820d 100644
--- a/common/src/com/android/tv/common/BaseSingletons.java
+++ b/common/src/com/android/tv/common/BaseSingletons.java
@@ -18,15 +18,28 @@
 
 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 HasCloudEpgFlags, HasBuildType, HasConcurrentDvrPlaybackFlags {
+public interface BaseSingletons extends HasCloudEpgFlags, HasBuildType {
 
+    /*
+     * Do not add any new methods here.
+     *
+     * To move a getter to Injection.
+     *  1. Make a type injectable @Singleton.
+     *  2. Mark the getter here as deprecated.
+     *  3. Lazily inject the object in TvApplication.
+     *  4. Move easy usages of getters to injection instead.
+     *  5. Delete the method when all usages are migrated.
+     */
+
+    /* @deprecated use injection instead.  */
+    @Deprecated
     Clock getClock();
 
+    /* @deprecated use injection instead.  */
+    @Deprecated
     RecordingStorageStatusManager getRecordingStorageStatusManager();
 }
diff --git a/common/src/com/android/tv/common/buildtype/BuildTypeFactory.java b/common/src/com/android/tv/common/buildtype/BuildTypeFactory.java
new file mode 100644
index 0000000..706a603
--- /dev/null
+++ b/common/src/com/android/tv/common/buildtype/BuildTypeFactory.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.buildtype;
+
+import com.google.common.base.Supplier;
+
+import javax.inject.Inject;
+
+
+/** Factory for {@link HasBuildType.BuildType}.
+ *
+ * <p>Hardcoded to {@link HasBuildType.BuildType#AOSP}.
+ */
+public class BuildTypeFactory implements Supplier<HasBuildType> {
+    private static final HasBuildType INSTANCE = new AospBuildTypeProvider();
+
+    @Inject
+    public BuildTypeFactory() {}
+
+    public static HasBuildType create() {
+        return INSTANCE;
+    }
+
+    @Override
+    public HasBuildType get() {
+        return INSTANCE;
+    }
+}
\ No newline at end of file
diff --git a/common/src/com/android/tv/common/buildtype/BuildTypeModule.java b/common/src/com/android/tv/common/buildtype/BuildTypeModule.java
new file mode 100644
index 0000000..43f398d
--- /dev/null
+++ b/common/src/com/android/tv/common/buildtype/BuildTypeModule.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.buildtype;
+
+import dagger.Module;
+import dagger.Provides;
+import dagger.Reusable;
+
+/** Provides BuildType */
+@Module
+public class BuildTypeModule {
+    private static final HasBuildType.BuildType BUILD_TYPE =
+            BuildTypeFactory.create().getBuildType();
+
+    @Provides
+    @Reusable
+    HasBuildType.BuildType providesBuildType() {
+        return BUILD_TYPE;
+    }
+}
diff --git a/common/src/com/android/tv/common/buildtype/HasBuildType.java b/common/src/com/android/tv/common/buildtype/HasBuildType.java
index 7d5677c..addac07 100644
--- a/common/src/com/android/tv/common/buildtype/HasBuildType.java
+++ b/common/src/com/android/tv/common/buildtype/HasBuildType.java
@@ -30,5 +30,7 @@
         PROD
     }
 
+    /** @deprecated use injection instead. */
+    @Deprecated
     BuildType getBuildType();
 }
diff --git a/common/src/com/android/tv/common/compat/TvInputInfoCompat.java b/common/src/com/android/tv/common/compat/TvInputInfoCompat.java
index 685a3ed..2f06d94 100644
--- a/common/src/com/android/tv/common/compat/TvInputInfoCompat.java
+++ b/common/src/com/android/tv/common/compat/TvInputInfoCompat.java
@@ -45,13 +45,12 @@
 
     private final Context mContext;
     private final TvInputInfo mTvInputInfo;
-    private final boolean mAudioOnly;
+    private boolean mAudioOnly;
+    private boolean mAudioAttributeInit = false;
 
     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() {
@@ -59,6 +58,11 @@
     }
 
     public boolean isAudioOnly() {
+        // TODO(b/112938832): use tvInputInfo.isAudioOnly() when SDK is updated
+        if (!mAudioAttributeInit) {
+            mAudioOnly = Boolean.parseBoolean(getExtras().get(ATTRIBUTE_NAME_AUDIO_ONLY));
+            mAudioAttributeInit = true;
+        }
         return mAudioOnly;
     }
 
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
index ce59bfa..c247e78 100644
--- a/common/src/com/android/tv/common/compat/internal/recording_commands.proto
+++ b/common/src/com/android/tv/common/compat/internal/recording_commands.proto
@@ -19,6 +19,7 @@
 // package and should not be used outside it.
 
 syntax = "proto3";
+
 package android.tv.common.compat.internal;
 
 option java_outer_classname = "RecordingCommands";
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
index 68db5dd..fffa62a 100644
--- a/common/src/com/android/tv/common/compat/internal/recording_events.proto
+++ b/common/src/com/android/tv/common/compat/internal/recording_events.proto
@@ -18,6 +18,7 @@
 // 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";
@@ -46,4 +47,3 @@
   // 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
index d586770..b69d487 100644
--- a/common/src/com/android/tv/common/compat/internal/tif_commands.proto
+++ b/common/src/com/android/tv/common/compat/internal/tif_commands.proto
@@ -19,6 +19,7 @@
 // package and should not be used outside it.
 
 syntax = "proto3";
+
 package android.tv.common.compat.internal;
 
 option java_outer_classname = "Commands";
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
index 6e71ae1..b15a884 100644
--- a/common/src/com/android/tv/common/compat/internal/tif_events.proto
+++ b/common/src/com/android/tv/common/compat/internal/tif_events.proto
@@ -18,6 +18,7 @@
 // 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";
diff --git a/common/src/com/android/tv/common/customization/CustomizationManager.java b/common/src/com/android/tv/common/customization/CustomizationManager.java
index 09ecaef..5a29d7c 100644
--- a/common/src/com/android/tv/common/customization/CustomizationManager.java
+++ b/common/src/com/android/tv/common/customization/CustomizationManager.java
@@ -97,8 +97,8 @@
 
     /**
      * Returns {@code true} if there's a customization package installed and it specifies built-in
-     * tuner devices are available. The built-in tuner should support DVB API to be recognized by
-     * Live TV.
+     * tuner devices are available. The built-in tuner should support DVB API to be recognized by TV
+     * app.
      */
     public static boolean hasLinuxDvbBuiltInTuner(Context context) {
         if (sHasLinuxDvbBuiltInTuner == null) {
@@ -156,11 +156,26 @@
 
     private static String getCustomizationPackageName(Context context) {
         if (sCustomizationPackage == null) {
+            sCustomizationPackage = "";
             List<PackageInfo> packageInfos =
                     context.getPackageManager()
                             .getPackagesHoldingPermissions(CUSTOMIZE_PERMISSIONS, 0);
-            sCustomizationPackage = packageInfos.size() == 0 ? "" : packageInfos.get(0).packageName;
+            if (packageInfos.size() != 0) {
+                /** Iterate through all packages returning the first vendor customizer */
+                for (PackageInfo packageInfo : packageInfos) {
+                    if (packageInfo.packageName.startsWith("com.android") == false) {
+                        sCustomizationPackage = packageInfo.packageName;
+                        break;
+                    }
+                }
+
+                /** If no vendor package found, return first in the list */
+                if (sCustomizationPackage == "") {
+                    sCustomizationPackage = packageInfos.get(0).packageName;
+                }
+            }
         }
+
         return sCustomizationPackage;
     }
 
diff --git a/common/src/com/android/tv/common/dagger/ApplicationModule.java b/common/src/com/android/tv/common/dagger/ApplicationModule.java
index 4655f77..be9cf88 100644
--- a/common/src/com/android/tv/common/dagger/ApplicationModule.java
+++ b/common/src/com/android/tv/common/dagger/ApplicationModule.java
@@ -21,8 +21,10 @@
 import android.os.Looper;
 import com.android.tv.common.dagger.annotations.ApplicationContext;
 import com.android.tv.common.dagger.annotations.MainLooper;
+import com.android.tv.common.util.Clock;
 import dagger.Module;
 import dagger.Provides;
+import dagger.Reusable;
 
 /**
  * Provides application-scope qualifiers for the {@link Application}, the application context, and
@@ -57,4 +59,10 @@
     ContentResolver provideContentResolver() {
         return mApplication.getContentResolver();
     }
+
+    @Provides
+    @Reusable
+    static Clock providesClock() {
+        return Clock.SYSTEM;
+    }
 }
diff --git a/common/src/com/android/tv/common/dagger/init/SafePreDaggerInitializer.java b/common/src/com/android/tv/common/dagger/init/SafePreDaggerInitializer.java
new file mode 100644
index 0000000..9465d92
--- /dev/null
+++ b/common/src/com/android/tv/common/dagger/init/SafePreDaggerInitializer.java
@@ -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.
+ */
+package com.android.tv.common.dagger.init;
+
+import android.content.Context;
+import android.util.Log;
+
+/**
+ * Initializes objects one time only.
+ *
+ * <p>This is needed because ContentProviders can be created before Application.onCreate
+ */
+public final class SafePreDaggerInitializer {
+    private interface Initialize {
+        void init(Context context);
+    }
+
+    private static final String TAG = "SafePreDaggerInitializer";
+
+    private static boolean initialized = false;
+    private static Context oldContext;
+
+    private static final Initialize[] sList =
+            new Initialize[] {
+                /* Begin_AOSP_Comment_Out
+                com.google.android.libraries.phenotype.client.PhenotypeContext::setContext
+                End_AOSP_Comment_Out */
+            };
+
+    public static synchronized void init(Context context) {
+        if (!initialized) {
+            for (Initialize i : sList) {
+                i.init(context);
+            }
+            oldContext = context;
+            initialized = true;
+        } else if (oldContext != context) {
+            Log.w(
+                    TAG,
+                    "init called more than once, skipping. Old context was "
+                            + oldContext
+                            + " new context is "
+                            + context);
+        }
+    }
+
+    private SafePreDaggerInitializer() {}
+}
diff --git a/common/src/com/android/tv/common/dev/DeveloperPreference.java b/common/src/com/android/tv/common/dev/DeveloperPreference.java
new file mode 100644
index 0000000..b1c401b
--- /dev/null
+++ b/common/src/com/android/tv/common/dev/DeveloperPreference.java
@@ -0,0 +1,132 @@
+/*
+ * 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.dev;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+
+/** Preferences available to developers */
+public abstract class DeveloperPreference<T> {
+
+    private static final String PREFERENCE_FILE_NAME =
+            "com.android.tv.common.dev.DeveloperPreference";
+
+    /**
+     * Create a boolean developer preference.
+     *
+     * @param key the developer setting key.
+     * @param defaultValue the value to return if the setting is undefined or empty.
+     */
+    public static DeveloperPreference<Boolean> create(String key, boolean defaultValue) {
+        return new DeveloperBooleanPreference(key, defaultValue);
+    }
+
+    @VisibleForTesting
+    static final SharedPreferences getPreferences(Context context) {
+        return context.getSharedPreferences(PREFERENCE_FILE_NAME, Context.MODE_PRIVATE);
+    }
+
+    /**
+     * Create a int developer preference.
+     *
+     * @param key the developer setting key.
+     * @param defaultValue the value to return if the setting is undefined or empty.
+     */
+    public static DeveloperPreference<Integer> create(String key, int defaultValue) {
+        return new DeveloperIntegerPreference(key, defaultValue);
+    }
+
+    final String mKey;
+    final T mDefaultValue;
+    private T mValue;
+
+    private DeveloperPreference(String key, T defaultValue) {
+        mKey = key;
+        mValue = null;
+        mDefaultValue = defaultValue;
+    }
+
+    /** Set the value. */
+    public final void set(Context context, T value) {
+        mValue = value;
+        storeValue(context, value);
+    }
+
+    protected abstract void storeValue(Context context, T value);
+
+    /** Get the current value, or the default if the value is not set. */
+    public final T get(Context context) {
+        mValue = getStoredValue(context);
+        return mValue;
+    }
+
+    /** Get the current value, or the default if the value is not set or context is null. */
+    public final T getDefaultIfContextNull(@Nullable Context context) {
+        return context == null ? mDefaultValue : getStoredValue(context);
+    }
+
+    protected abstract T getStoredValue(Context context);
+
+    /**
+     * Clears the current value.
+     *
+     * <p>Future calls to {@link #get(Context)} will return the default value.
+     */
+    public final void clear(Context context) {
+        getPreferences(context).edit().remove(mKey);
+    }
+
+    @Override
+    public final String toString() {
+        return "[" + mKey + "]=" + mValue + " Default value : " + mDefaultValue;
+    }
+
+    private static final class DeveloperBooleanPreference extends DeveloperPreference<Boolean> {
+
+        private DeveloperBooleanPreference(String key, Boolean defaultValue) {
+            super(key, defaultValue);
+        }
+
+        @Override
+        public void storeValue(Context context, Boolean value) {
+            getPreferences(context).edit().putBoolean(mKey, value).apply();
+        }
+
+        @Override
+        public Boolean getStoredValue(Context context) {
+            return getPreferences(context).getBoolean(mKey, mDefaultValue);
+        }
+    }
+
+    private static final class DeveloperIntegerPreference extends DeveloperPreference<Integer> {
+
+        private DeveloperIntegerPreference(String key, Integer defaultValue) {
+            super(key, defaultValue);
+        }
+
+        @Override
+        protected void storeValue(Context context, Integer value) {
+            getPreferences(context).edit().putInt(mKey, value).apply();
+        }
+
+        @Override
+        protected Integer getStoredValue(Context context) {
+            return getPreferences(context).getInt(mKey, mDefaultValue);
+        }
+    }
+}
diff --git a/common/src/com/android/tv/common/dev/DeveloperPreferences.java b/common/src/com/android/tv/common/dev/DeveloperPreferences.java
new file mode 100644
index 0000000..9c83b64
--- /dev/null
+++ b/common/src/com/android/tv/common/dev/DeveloperPreferences.java
@@ -0,0 +1,71 @@
+/*
+ * 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.dev;
+
+/** A class about the constants for TV Developer preferences. */
+public final class DeveloperPreferences {
+
+    /**
+     * Allow Google Analytics for eng builds.
+     *
+     * <p>Defaults to {@code false}.
+     */
+    public static final DeveloperPreference<Boolean> ALLOW_ANALYTICS_IN_ENG =
+            DeveloperPreference.create("tv_allow_analytics_in_eng", false);
+
+    /**
+     * Allow Strict mode for debug builds.
+     *
+     * <p>Defaults to {@code true}.
+     */
+    public static final DeveloperPreference<Boolean> ALLOW_STRICT_MODE =
+            DeveloperPreference.create("tv_allow_strict_mode", true);
+
+    /**
+     * When true {@link android.view.KeyEvent}s are logged.
+     *
+     * <p>Defaults to {@code false}.
+     */
+    public static final DeveloperPreference<Boolean> LOG_KEYEVENT =
+            DeveloperPreference.create("tv_log_keyevent", false);
+
+    /**
+     * When true debug keys are used.
+     *
+     * <p>Defaults to {@code false}.
+     */
+    public static final DeveloperPreference<Boolean> USE_DEBUG_KEYS =
+            DeveloperPreference.create("tv_use_debug_keys", false);
+
+    /**
+     * Send {@link com.android.tv.analytics.Tracker} information.
+     *
+     * <p>Defaults to {@code true}.
+     */
+    public static final DeveloperPreference<Boolean> USE_TRACKER =
+            DeveloperPreference.create("tv_use_tracker", true);
+
+    /**
+     * Maximum buffer size in MegaBytes.
+     *
+     * <p>Defaults to 2MB.
+     */
+    public static final DeveloperPreference<Integer> MAX_BUFFER_SIZE_MBYTES =
+            DeveloperPreference.create("tv.tuner.buffersize_mbytes", 2 * 1024);
+
+    private DeveloperPreferences() {}
+}
diff --git a/common/src/com/android/tv/common/experiments/ExperimentFlag.java b/common/src/com/android/tv/common/experiments/ExperimentFlag.java
deleted file mode 100644
index b8370ad..0000000
--- a/common/src/com/android/tv/common/experiments/ExperimentFlag.java
+++ /dev/null
@@ -1,96 +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.experiments;
-
-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;
-        }
-    }
-
-    @VisibleForTesting
-    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/ExperimentLoader.java b/common/src/com/android/tv/common/experiments/ExperimentLoader.java
deleted file mode 100644
index 5f012e1..0000000
--- a/common/src/com/android/tv/common/experiments/ExperimentLoader.java
+++ /dev/null
@@ -1,28 +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.experiments;
-
-import android.content.Context;
-
-/** Used to sync {@link ExperimentFlag}s. */
-public class ExperimentLoader {
-
-    /** Starts a background task to update {@link ExperimentFlag}s */
-    public void asyncRefreshExperiments(Context context) {
-        // Override for your experiment system
-    }
-}
diff --git a/common/src/com/android/tv/common/experiments/Experiments.java b/common/src/com/android/tv/common/experiments/Experiments.java
deleted file mode 100644
index 9bfdb54..0000000
--- a/common/src/com/android/tv/common/experiments/Experiments.java
+++ /dev/null
@@ -1,64 +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.experiments;
-
-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.
- *
- * <p>This file is maintained by hand.
- */
-public final class Experiments {
-    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);
-
-    /**
-     * Allow developer features such as the dev menu and other aids.
-     *
-     * <p>These features are available to select users(aka fishfooders) on production builds.
-     */
-    public static final ExperimentFlag<Boolean> ENABLE_DEVELOPER_FEATURES =
-            ExperimentFlag.createFlag(
-// AOSP_Comment_Out                     LiveChannels::enableDeveloperFeatures,
-                    BuildConfig.ENG);
-
-    /**
-     * Allow QA features.
-     *
-     * <p>These features must be carefully limited, keeping QA differences to a minimum.
-     *
-     * <p>These features are available to select users(aka QA) on production builds.
-     */
-    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/CommonFeatures.java b/common/src/com/android/tv/common/feature/CommonFeatures.java
index 04052a7..abe4c1d 100644
--- a/common/src/com/android/tv/common/feature/CommonFeatures.java
+++ b/common/src/com/android/tv/common/feature/CommonFeatures.java
@@ -23,12 +23,14 @@
 
 import android.content.Context;
 import android.util.Log;
+
 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.
+ * List of {@link Feature} that affect more than just the TV app.
  *
  * <p>Remove the {@code Feature} once it is launched.
  */
@@ -52,7 +54,7 @@
      * <p>Enables dvr recording regardless of storage status.
      */
     public static final Feature FORCE_RECORDING_UNTIL_NO_SPACE =
-            PropertyFeature.create("force_recording_until_no_space", false);
+            DeveloperPreferenceFeature.create("force_recording_until_no_space", false);
 
     /** Show postal code fragment before channel scan. */
     public static final Feature ENABLE_CLOUD_EPG_REGION =
diff --git a/common/src/com/android/tv/common/feature/DeveloperPreferenceFeature.java b/common/src/com/android/tv/common/feature/DeveloperPreferenceFeature.java
new file mode 100644
index 0000000..1f98547
--- /dev/null
+++ b/common/src/com/android/tv/common/feature/DeveloperPreferenceFeature.java
@@ -0,0 +1,59 @@
+/*
+ * 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.feature;
+
+import android.content.Context;
+
+import com.android.tv.common.dev.DeveloperPreference;
+
+/** A {@link Feature} based on {@link DeveloperPreference<Boolean>}. */
+public class DeveloperPreferenceFeature implements Feature {
+
+    private final DeveloperPreference<Boolean> mPreference;
+
+    /**
+     * Create a developer preference feature.
+     *
+     * @param key the developer setting key.
+     * @param defaultValue the value to return if the setting is undefined or empty.
+     */
+    public static DeveloperPreferenceFeature create(String key, boolean defaultValue) {
+        return from(DeveloperPreference.create(key, defaultValue));
+    }
+
+    /**
+     * Create a developer preference feature from an exiting {@link DeveloperPreference<Boolean>}.
+     */
+    public static DeveloperPreferenceFeature from(
+            DeveloperPreference<Boolean> developerPreference) {
+        return new DeveloperPreferenceFeature(developerPreference);
+    }
+
+    private DeveloperPreferenceFeature(DeveloperPreference<Boolean> mPreference) {
+        this.mPreference = mPreference;
+    }
+
+    @Override
+    public boolean isEnabled(Context context) {
+        return mPreference.get(context);
+    }
+
+    @Override
+    public String toString() {
+        return mPreference.toString();
+    }
+}
diff --git a/common/src/com/android/tv/common/feature/ExperimentFeature.java b/common/src/com/android/tv/common/feature/ExperimentFeature.java
deleted file mode 100644
index 820eda4..0000000
--- a/common/src/com/android/tv/common/feature/ExperimentFeature.java
+++ /dev/null
@@ -1,44 +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.common.feature;
-
-import android.content.Context;
-import com.android.tv.common.experiments.ExperimentFlag;
-
-/** A {@link Feature} base on an {@link ExperimentFlag}. */
-public final class ExperimentFeature implements Feature {
-
-    public static Feature from(ExperimentFlag<Boolean> flag) {
-        return new ExperimentFeature(flag);
-    }
-
-    private final ExperimentFlag<Boolean> mFlag;
-
-    private ExperimentFeature(ExperimentFlag<Boolean> flag) {
-        mFlag = flag;
-    }
-
-    @Override
-    public boolean isEnabled(Context context) {
-        return mFlag.get();
-    }
-
-    @Override
-    public String toString() {
-        return "ExperimentFeature for " + mFlag;
-    }
-}
diff --git a/common/src/com/android/tv/common/feature/FeatureUtils.java b/common/src/com/android/tv/common/feature/FeatureUtils.java
index aaed6c8..e6192cd 100644
--- a/common/src/com/android/tv/common/feature/FeatureUtils.java
+++ b/common/src/com/android/tv/common/feature/FeatureUtils.java
@@ -17,7 +17,6 @@
 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;
 
@@ -71,23 +70,6 @@
             }
         };
     }
-    /**
-     * 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}.
diff --git a/common/src/com/android/tv/common/feature/Model.java b/common/src/com/android/tv/common/feature/Model.java
index 7aa5148..450cd21 100644
--- a/common/src/com/android/tv/common/feature/Model.java
+++ b/common/src/com/android/tv/common/feature/Model.java
@@ -21,10 +21,11 @@
 /** Holder for {@link android.os.Build#MODEL} features. */
 public interface Model {
 
+    ModelFeature ARCHER = new ModelFeature("Archer");
     ModelFeature NEXUS_PLAYER = new ModelFeature("Nexus Player");
 
     /** True when the {@link android.os.Build#MODEL} equals the {@code model} given. */
-    public static final class ModelFeature implements Feature {
+    final class ModelFeature implements Feature {
         private final String mModel;
 
         private ModelFeature(String model) {
diff --git a/common/src/com/android/tv/common/feature/PermissionFeature.java b/common/src/com/android/tv/common/feature/PermissionFeature.java
new file mode 100644
index 0000000..0261178
--- /dev/null
+++ b/common/src/com/android/tv/common/feature/PermissionFeature.java
@@ -0,0 +1,38 @@
+/*
+ * 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.feature;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+/** A feature that is only available when {@code permissionName} is granted. */
+public class PermissionFeature implements Feature {
+
+    public static final PermissionFeature DVB_DEVICE_PERMISSION =
+            new PermissionFeature("android.permission.DVB_DEVICE");
+
+    private final String permissionName;
+
+    private PermissionFeature(String permissionName) {
+        this.permissionName = permissionName;
+    }
+
+    @Override
+    public boolean isEnabled(Context context) {
+        return context.checkSelfPermission(permissionName) == PackageManager.PERMISSION_GRANTED;
+    }
+}
diff --git a/common/src/com/android/tv/common/feature/Sdk.java b/common/src/com/android/tv/common/feature/Sdk.java
index 4b0a925..54bc1bb 100644
--- a/common/src/com/android/tv/common/feature/Sdk.java
+++ b/common/src/com/android/tv/common/feature/Sdk.java
@@ -29,8 +29,6 @@
 
     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;
diff --git a/common/src/com/android/tv/common/flags/BackendKnobsFlags.java b/common/src/com/android/tv/common/flags/BackendKnobsFlags.java
index 69bac7a..c6272c0 100644
--- a/common/src/com/android/tv/common/flags/BackendKnobsFlags.java
+++ b/common/src/com/android/tv/common/flags/BackendKnobsFlags.java
@@ -26,18 +26,12 @@
      */
     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();
 
diff --git a/common/src/com/android/tv/common/flags/CloudEpgFlags.java b/common/src/com/android/tv/common/flags/CloudEpgFlags.java
index ab4c6a1..48b950b 100755
--- a/common/src/com/android/tv/common/flags/CloudEpgFlags.java
+++ b/common/src/com/android/tv/common/flags/CloudEpgFlags.java
@@ -29,6 +29,6 @@
     /** Is the device in a region supported by Cloud Epg */
     boolean supportedRegion();
 
-    /** List of input ids that Live TV will update their EPG. */
+    /** List of input ids that the TV app 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
index 1afff79..42892e0 100755
--- a/common/src/com/android/tv/common/flags/ConcurrentDvrPlaybackFlags.java
+++ b/common/src/com/android/tv/common/flags/ConcurrentDvrPlaybackFlags.java
@@ -26,9 +26,6 @@
      */
     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/DvrFlags.java b/common/src/com/android/tv/common/flags/DvrFlags.java
new file mode 100755
index 0000000..2e1d531
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/DvrFlags.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;
+
+/** DVR flags */
+public interface DvrFlags {
+
+    /**
+     * 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();
+
+    /** Allow user to customize timings of program recordings. */
+    boolean startEarlyEndLateEnabled();
+
+    /** Store and use the video aspect ratio in recordings. */
+    boolean storeVideoAspectRatio();
+}
diff --git a/common/src/com/android/tv/common/flags/LegacyFlags.java b/common/src/com/android/tv/common/flags/LegacyFlags.java
new file mode 100755
index 0000000..dbccf70
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/LegacyFlags.java
@@ -0,0 +1,37 @@
+/*
+ * 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;
+
+/** Legacy flags */
+public interface LegacyFlags {
+
+    /**
+     * 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 Developer Features */
+    boolean enableDeveloperFeatures();
+
+    /** Enable QA Features */
+    boolean enableQaFeatures();
+
+    /** Enable Unrated Content Settings */
+    boolean enableUnratedContentSettings();
+}
diff --git a/common/src/com/android/tv/common/flags/MessagesFlags.java b/common/src/com/android/tv/common/flags/MessagesFlags.java
new file mode 100755
index 0000000..b5411d7
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/MessagesFlags.java
@@ -0,0 +1,37 @@
+/*
+ * 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;
+
+/**
+ * Message flags.
+ *
+ * <p>Used to hide new messages until all translations are ready.
+ *
+ * <p>Production releases never include the messages protected by these flags.
+ */
+public interface MessagesFlags {
+
+    /**
+     * 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();
+
+    /** Use setup_sources_description2 */
+    boolean setupSourcesDescription2();
+}
diff --git a/common/src/com/android/tv/common/flags/StartupFlags.java b/common/src/com/android/tv/common/flags/StartupFlags.java
new file mode 100755
index 0000000..e6f6837
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/StartupFlags.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.common.flags;
+
+/** Flags for TV App startup */
+public interface StartupFlags {
+
+    /**
+     * 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();
+
+    /** InputId's that will not be warmed up on MainActivity creation. */
+    com.android.tv.common.flags.proto.TypedFeatures.StringListParam warmupInputidBlacklist();
+}
diff --git a/common/src/com/android/tv/common/flags/TunerFlags.java b/common/src/com/android/tv/common/flags/TunerFlags.java
index 5f899b9..5be7b79 100755
--- a/common/src/com/android/tv/common/flags/TunerFlags.java
+++ b/common/src/com/android/tv/common/flags/TunerFlags.java
@@ -26,9 +26,6 @@
      */
     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
index 4c88d08..73349be 100755
--- a/common/src/com/android/tv/common/flags/UiFlags.java
+++ b/common/src/com/android/tv/common/flags/UiFlags.java
@@ -15,7 +15,7 @@
  */
 package com.android.tv.common.flags;
 
-/** Flags for Live TV UI */
+/** Flags for TV app UI */
 public interface UiFlags {
 
     /**
@@ -26,6 +26,9 @@
      */
     boolean compiled();
 
+    /** Critic Ratings */
+    boolean enableCriticRatings();
+
     /**
      * Number of days to be shown by Recording History.
      *
@@ -35,7 +38,4 @@
 
     /** 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/HasConcurrentDvrPlaybackFlags.java b/common/src/com/android/tv/common/flags/has/HasConcurrentDvrPlaybackFlags.java
deleted file mode 100644
index b471087..0000000
--- a/common/src/com/android/tv/common/flags/has/HasConcurrentDvrPlaybackFlags.java
+++ /dev/null
@@ -1,30 +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.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/common/src/com/android/tv/common/flags/impl/DefaultBackendKnobsFlags.java b/common/src/com/android/tv/common/flags/impl/DefaultBackendKnobsFlags.java
index a189e47..cc6612f 100644
--- a/common/src/com/android/tv/common/flags/impl/DefaultBackendKnobsFlags.java
+++ b/common/src/com/android/tv/common/flags/impl/DefaultBackendKnobsFlags.java
@@ -25,23 +25,13 @@
     }
 
     @Override
-    public boolean enablePartialProgramFetch() {
-        return false;
-    }
-
-    @Override
     public long epgFetcherIntervalHour() {
         return 25;
     }
 
     @Override
-    public boolean fetchProgramsAsNeeded() {
-        return false;
-    }
-
-    @Override
     public long programGuideInitialFetchHours() {
-        return 8;
+        return 4;
     }
 
     @Override
diff --git a/common/src/com/android/tv/common/flags/impl/DefaultConcurrentDvrPlaybackFlags.java b/common/src/com/android/tv/common/flags/impl/DefaultConcurrentDvrPlaybackFlags.java
index 8d8c584..ee470ca 100644
--- a/common/src/com/android/tv/common/flags/impl/DefaultConcurrentDvrPlaybackFlags.java
+++ b/common/src/com/android/tv/common/flags/impl/DefaultConcurrentDvrPlaybackFlags.java
@@ -26,11 +26,6 @@
     }
 
     @Override
-    public boolean enabled() {
-        return false;
-    }
-
-    @Override
     public boolean onTuneUsesRecording() {
         return false;
     }
diff --git a/common/src/com/android/tv/common/flags/impl/DefaultDvrFlags.java b/common/src/com/android/tv/common/flags/impl/DefaultDvrFlags.java
new file mode 100644
index 0000000..09f7b4f
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/impl/DefaultDvrFlags.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.flags.impl;
+
+/** Flags for tuning non ui behavior. */
+public final class DefaultDvrFlags
+        implements com.android.tv.common.flags.DvrFlags {
+
+    @Override
+    public boolean compiled() {
+        return true;
+    }
+
+    @Override
+    public boolean startEarlyEndLateEnabled() {
+        return false;
+    }
+
+    @Override
+    public boolean storeVideoAspectRatio() {
+        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
index 4935236..2aaf446 100644
--- a/common/src/com/android/tv/common/flags/impl/DefaultFlagsModule.java
+++ b/common/src/com/android/tv/common/flags/impl/DefaultFlagsModule.java
@@ -21,6 +21,9 @@
 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.DvrFlags;
+import com.android.tv.common.flags.LegacyFlags;
+import com.android.tv.common.flags.StartupFlags;
 import com.android.tv.common.flags.TunerFlags;
 import com.android.tv.common.flags.UiFlags;
 
@@ -48,6 +51,24 @@
 
     @Provides
     @Reusable
+    DvrFlags provideDvrFlags() {
+        return new DefaultDvrFlags();
+    }
+
+    @Provides
+    @Reusable
+    LegacyFlags provideLegacyFlags() {
+        return DefaultLegacyFlags.DEFAULT;
+    }
+
+    @Provides
+    @Reusable
+    StartupFlags provideStartupFlags() {
+        return new DefaultStartupFlags();
+    }
+
+    @Provides
+    @Reusable
     TunerFlags provideTunerFlags() {
         return new DefaultTunerFlags();
     }
diff --git a/common/src/com/android/tv/common/flags/impl/DefaultLegacyFlags.java b/common/src/com/android/tv/common/flags/impl/DefaultLegacyFlags.java
new file mode 100644
index 0000000..5214241
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/impl/DefaultLegacyFlags.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.common.flags.impl;
+
+import com.google.auto.value.AutoValue;
+import com.android.tv.common.flags.LegacyFlags;
+
+/** Default {@link LegacyFlags}. */
+@AutoValue
+public abstract class DefaultLegacyFlags implements LegacyFlags {
+    public static final DefaultLegacyFlags DEFAULT = DefaultLegacyFlags.builder().build();
+
+    public static Builder builder() {
+        return new AutoValue_DefaultLegacyFlags.Builder()
+                .compiled(true)
+                .enableDeveloperFeatures(false)
+                .enableQaFeatures(false)
+                .enableUnratedContentSettings(false);
+    }
+
+    /** Builder for {@link LegacyFlags} */
+    @AutoValue.Builder
+    public abstract static class Builder {
+        public abstract Builder compiled(boolean value);
+
+        public abstract Builder enableDeveloperFeatures(boolean value);
+
+        public abstract Builder enableQaFeatures(boolean value);
+
+        public abstract Builder enableUnratedContentSettings(boolean value);
+
+        public abstract DefaultLegacyFlags build();
+    }
+}
diff --git a/common/src/com/android/tv/common/flags/impl/DefaultMessagesFlags.java b/common/src/com/android/tv/common/flags/impl/DefaultMessagesFlags.java
new file mode 100644
index 0000000..091f422
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/impl/DefaultMessagesFlags.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.impl;
+
+/**
+ * Default flag values for {@link
+ * com.android.tv.common.flags.MessagesFlags}.
+ */
+public final class DefaultMessagesFlags
+        implements com.android.tv.common.flags.MessagesFlags {
+
+    @Override
+    public boolean compiled() {
+        return true;
+    }
+
+    @Override
+    public boolean setupSourcesDescription2() {
+        return false;
+    }
+}
diff --git a/common/src/com/android/tv/common/flags/impl/DefaultStartupFlags.java b/common/src/com/android/tv/common/flags/impl/DefaultStartupFlags.java
new file mode 100644
index 0000000..3eb6edc
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/impl/DefaultStartupFlags.java
@@ -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.
+ */
+package com.android.tv.common.flags.impl;
+
+import com.android.tv.common.flags.proto.TypedFeatures.StringListParam;
+import com.android.tv.common.flags.StartupFlags;
+
+/** Default {@link StartupFlags} */
+public class DefaultStartupFlags implements StartupFlags {
+    @Override
+    public boolean compiled() {
+        return true;
+    }
+
+    @Override
+    public StringListParam warmupInputidBlacklist() {
+        return StringListParam.getDefaultInstance();
+    }
+}
diff --git a/common/src/com/android/tv/common/flags/impl/DefaultTunerFlags.java b/common/src/com/android/tv/common/flags/impl/DefaultTunerFlags.java
index 195953b..2d12e36 100644
--- a/common/src/com/android/tv/common/flags/impl/DefaultTunerFlags.java
+++ b/common/src/com/android/tv/common/flags/impl/DefaultTunerFlags.java
@@ -26,11 +26,6 @@
     }
 
     @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
index fce4585..2746485 100644
--- a/common/src/com/android/tv/common/flags/impl/DefaultUiFlags.java
+++ b/common/src/com/android/tv/common/flags/impl/DefaultUiFlags.java
@@ -17,7 +17,7 @@
 
 import com.android.tv.common.flags.UiFlags;
 
-/** Default Flags for Live TV UI */
+/** Default Flags for TV app UI */
 public class DefaultUiFlags implements UiFlags {
 
     @Override
@@ -26,17 +26,17 @@
     }
 
     @Override
+    public boolean enableCriticRatings() {
+        return false;
+    }
+
+    @Override
     public boolean uhideLauncher() {
         return false;
     }
 
     @Override
-    public boolean useLeanbackPinPicker() {
-        return false;
-    }
-
-    @Override
     public long maxHistoryDays() {
-        return 7;
+        return 0;
     }
 }
diff --git a/common/src/com/android/tv/common/flags/impl/SettableFlagsModule.java b/common/src/com/android/tv/common/flags/impl/SettableFlagsModule.java
new file mode 100644
index 0000000..b188158
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/impl/SettableFlagsModule.java
@@ -0,0 +1,91 @@
+/*
+ * 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.DvrFlags;
+import com.android.tv.common.flags.LegacyFlags;
+import com.android.tv.common.flags.StartupFlags;
+import com.android.tv.common.flags.TunerFlags;
+import com.android.tv.common.flags.UiFlags;
+
+/** Provides public fields for each flag so they can be changed before injection. */
+@Module
+public class SettableFlagsModule {
+
+    public DefaultBackendKnobsFlags backendKnobsFlags = new DefaultBackendKnobsFlags();
+    public DefaultCloudEpgFlags cloudEpgFlags = new DefaultCloudEpgFlags();
+    public DefaultConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags =
+            new DefaultConcurrentDvrPlaybackFlags();
+    public DefaultDvrFlags dvrFlags = new DefaultDvrFlags();
+    public DefaultLegacyFlags legacyFlags = DefaultLegacyFlags.DEFAULT;
+    public DefaultStartupFlags startupFlags = new DefaultStartupFlags();
+    public DefaultTunerFlags tunerFlags = new DefaultTunerFlags();
+    public DefaultUiFlags uiFlags = new DefaultUiFlags();
+
+    @Provides
+    @Reusable
+    BackendKnobsFlags provideBackendKnobsFlags() {
+        return backendKnobsFlags;
+    }
+
+    @Provides
+    @Reusable
+    CloudEpgFlags provideCloudEpgFlags() {
+        return cloudEpgFlags;
+    }
+
+    @Provides
+    @Reusable
+    ConcurrentDvrPlaybackFlags provideConcurrentDvrPlaybackFlags() {
+        return concurrentDvrPlaybackFlags;
+    }
+
+    @Provides
+    @Reusable
+    DvrFlags provideDvrFlags() {
+        return dvrFlags;
+    }
+
+    @Provides
+    @Reusable
+    LegacyFlags provideLegacyFlags() {
+        return legacyFlags;
+    }
+
+    @Provides
+    @Reusable
+    StartupFlags provideStartupFlags() {
+        return startupFlags;
+    }
+
+    @Provides
+    @Reusable
+    TunerFlags provideTunerFlags() {
+        return tunerFlags;
+    }
+
+    @Provides
+    @Reusable
+    UiFlags provideUiFlags() {
+        return uiFlags;
+    }
+}
diff --git a/common/src/com/android/tv/common/flags/proto/typed-features.proto b/common/src/com/android/tv/common/flags/proto/typed-features.proto
new file mode 100644
index 0000000..855d731
--- /dev/null
+++ b/common/src/com/android/tv/common/flags/proto/typed-features.proto
@@ -0,0 +1,20 @@
+syntax = "proto2";
+
+package android.tv.common.flags;
+
+option java_outer_classname = "TypedFeatures";
+option java_package = "com.android.tv.common.flags.proto";
+
+// These messages are to specify feature params that are a list of integers.
+message Int32ListParam {
+  repeated int32 element = 1;
+}
+
+message Int64ListParam {
+  repeated int64 element = 1;
+}
+
+// This message is to specify feature params that are a list of strings.
+message StringListParam {
+  repeated string element = 1;
+}
diff --git a/common/src/com/android/tv/common/recording/RecordingStorageStatusManager.java b/common/src/com/android/tv/common/recording/RecordingStorageStatusManager.java
index 0fb864b..3552a66 100644
--- a/common/src/com/android/tv/common/recording/RecordingStorageStatusManager.java
+++ b/common/src/com/android/tv/common/recording/RecordingStorageStatusManager.java
@@ -28,8 +28,11 @@
 import android.support.annotation.IntDef;
 import android.support.annotation.WorkerThread;
 import android.util.Log;
+
 import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.dagger.annotations.ApplicationContext;
 import com.android.tv.common.feature.CommonFeatures;
+
 import java.io.File;
 import java.io.IOException;
 import java.lang.annotation.Retention;
@@ -38,10 +41,13 @@
 import java.util.Set;
 import java.util.concurrent.CopyOnWriteArraySet;
 
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
 /** Signals DVR storage status change such as plugging/unplugging. */
+@Singleton
 public class RecordingStorageStatusManager {
     private static final String TAG = "RecordingStorageStatusManager";
-    private static final boolean DEBUG = false;
 
     /** Minimum storage size to support DVR */
     public static final long MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES = 50 * 1024 * 1024 * 1024L; // 50GB
@@ -143,7 +149,8 @@
      *
      * @param context {@link Context}
      */
-    public RecordingStorageStatusManager(final Context context) {
+    @Inject
+    public RecordingStorageStatusManager(@ApplicationContext Context context) {
         mContext = context;
         mMountedStorageStatus = getStorageStatusInternal();
         mStorageValid = mMountedStorageStatus.isValidForDvr();
diff --git a/common/src/com/android/tv/common/singletons/HasTvInputId.java b/common/src/com/android/tv/common/singletons/HasTvInputId.java
index 4bc0a21..49cf3d2 100644
--- a/common/src/com/android/tv/common/singletons/HasTvInputId.java
+++ b/common/src/com/android/tv/common/singletons/HasTvInputId.java
@@ -18,8 +18,8 @@
 /**
  * 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.
+ * <p>This is used buy both the tuner to get its input id and by the TV app to get the embedded
+ * tuner input id.
  */
 public interface HasTvInputId {
 
diff --git a/common/src/com/android/tv/common/support/tvprovider/README.md b/common/src/com/android/tv/common/support/tvprovider/README.md
new file mode 100644
index 0000000..a24dc28
--- /dev/null
+++ b/common/src/com/android/tv/common/support/tvprovider/README.md
@@ -0,0 +1,6 @@
+## support provider
+
+This is preview code destined to be put in androidx.tvprovider.media.tv
+
+
+All classes here must have an associated bug to move to androidx
diff --git a/common/src/com/android/tv/common/support/tvprovider/TvContractCompatX.java b/common/src/com/android/tv/common/support/tvprovider/TvContractCompatX.java
new file mode 100644
index 0000000..353e342
--- /dev/null
+++ b/common/src/com/android/tv/common/support/tvprovider/TvContractCompatX.java
@@ -0,0 +1,108 @@
+/*
+ * 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.support.tvprovider;
+
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import androidx.tvprovider.media.tv.TvContractCompat;
+
+/**
+ * Extensions to the contract between the TV provider and applications. Contains definitions for the
+ * supported URIs and columns.
+ *
+ * <p>TODO(b/126921088): move this to androidx.
+ */
+public final class TvContractCompatX {
+
+    /**
+     * Builds a URI that points to a specific channel.
+     *
+     * @param inputPackage the package of the input.
+     * @param internalProviderId the internal provider id
+     */
+    public static Uri buildChannelUri(
+            @Nullable String inputPackage, @Nullable String internalProviderId) {
+        Uri.Builder uri = TvContractCompat.Channels.CONTENT_URI.buildUpon();
+        if (inputPackage != null) {
+            uri.appendQueryParameter("package", inputPackage);
+        }
+        if (internalProviderId != null) {
+            uri.appendQueryParameter(
+                    TvContractCompat.Channels.COLUMN_INTERNAL_PROVIDER_ID, internalProviderId);
+        }
+        return uri.build();
+    }
+
+    /**
+     * Builds a URI that points to all programs on a given channel.
+     *
+     * @param inputPackage the package of the input.
+     * @param internalProviderId the internal provider id
+     */
+    public static Uri buildProgramsUriForChannel(
+            @Nullable String inputPackage, @Nullable String internalProviderId) {
+        Uri.Builder uri = TvContractCompat.Programs.CONTENT_URI.buildUpon();
+        if (inputPackage != null) {
+            uri.appendQueryParameter("package", inputPackage);
+        }
+        if (internalProviderId != null) {
+            uri.appendQueryParameter(
+                    TvContractCompat.Channels.COLUMN_INTERNAL_PROVIDER_ID, internalProviderId);
+        }
+        return uri.build();
+    }
+
+    /**
+     * Builds a URI that points to programs on a specific channel whose schedules overlap with the
+     * given time frame.
+     *
+     * @param inputPackage the package of the input.
+     * @param internalProviderId the internal provider id
+     * @param startTime The start time used to filter programs. The returned programs should have
+     *     {@link TvContractCompat.Programs#COLUMN_END_TIME_UTC_MILLIS} that is greater than this
+     *     time.
+     * @param endTime The end time used to filter programs. The returned programs should have {@link
+     *     TvContractCompat.Programs#COLUMN_START_TIME_UTC_MILLIS} that is less than this time.
+     */
+    public static Uri buildProgramsUriForChannel(
+            @Nullable String inputPackage,
+            @Nullable String internalProviderId,
+            long startTime,
+            long endTime) {
+        return buildProgramsUriForChannel(inputPackage, internalProviderId)
+                .buildUpon()
+                .appendQueryParameter(TvContractCompat.PARAM_START_TIME, String.valueOf(startTime))
+                .appendQueryParameter(TvContractCompat.PARAM_END_TIME, String.valueOf(endTime))
+                .build();
+    }
+
+    /**
+     * Builds a URI that points to programs whose schedules overlap with the given time frame.
+     *
+     * @param startTime The start time used to filter programs. The returned programs should have
+     *     {@link TvContractCompat.Programs#COLUMN_END_TIME_UTC_MILLIS} that is greater than this
+     *     time.
+     * @param endTime The end time used to filter programs. The returned programs should have {@link
+     *     TvContractCompat.Programs#COLUMN_START_TIME_UTC_MILLIS} that is less than this time.
+     */
+    public static Uri buildProgramsUri(long startTime, long endTime) {
+        return TvContractCompat.Programs.CONTENT_URI
+                .buildUpon()
+                .appendQueryParameter(TvContractCompat.PARAM_START_TIME, String.valueOf(startTime))
+                .appendQueryParameter(TvContractCompat.PARAM_END_TIME, String.valueOf(endTime))
+                .build();
+    }
+}
diff --git a/common/src/com/android/tv/common/ui/setup/SetupGuidedStepFragment.java b/common/src/com/android/tv/common/ui/setup/SetupGuidedStepFragment.java
index 3c76c26..2a6ceec 100644
--- a/common/src/com/android/tv/common/ui/setup/SetupGuidedStepFragment.java
+++ b/common/src/com/android/tv/common/ui/setup/SetupGuidedStepFragment.java
@@ -19,11 +19,11 @@
 import static android.content.Context.ACCESSIBILITY_SERVICE;
 
 import android.os.Bundle;
-import android.support.v17.leanback.app.GuidedStepFragment;
-import android.support.v17.leanback.widget.GuidanceStylist;
-import android.support.v17.leanback.widget.GuidedAction;
-import android.support.v17.leanback.widget.GuidedActionsStylist;
-import android.support.v17.leanback.widget.VerticalGridView;
+import androidx.leanback.app.GuidedStepFragment;
+import androidx.leanback.widget.GuidanceStylist;
+import androidx.leanback.widget.GuidedAction;
+import androidx.leanback.widget.GuidedActionsStylist;
+import androidx.leanback.widget.VerticalGridView;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.AccessibilityDelegate;
@@ -53,9 +53,9 @@
             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
         View view = super.onCreateView(inflater, container, savedInstanceState);
         Bundle arguments = getArguments();
-        view.findViewById(android.support.v17.leanback.R.id.action_fragment_root)
+        view.findViewById(androidx.leanback.R.id.action_fragment_root)
                 .setPadding(0, 0, 0, 0);
-        mContentFragment = view.findViewById(android.support.v17.leanback.R.id.content_fragment);
+        mContentFragment = view.findViewById(androidx.leanback.R.id.content_fragment);
         LinearLayout.LayoutParams guidanceLayoutParams =
                 (LinearLayout.LayoutParams) mContentFragment.getLayoutParams();
         guidanceLayoutParams.weight = 0;
@@ -69,7 +69,7 @@
                     getResources()
                             .getDimensionPixelOffset(R.dimen.setup_done_button_container_width);
             // Guided actions list
-            View list = view.findViewById(android.support.v17.leanback.R.id.guidedactions_list);
+            View list = view.findViewById(androidx.leanback.R.id.guidedactions_list);
             MarginLayoutParams marginLayoutParams = (MarginLayoutParams) list.getLayoutParams();
             // Use content view to check layout direction while view is being created.
             if (getResources().getConfiguration().getLayoutDirection()
@@ -93,12 +93,12 @@
         gridView.setWindowAlignmentOffset(offset);
         gridView.setWindowAlignmentOffsetPercent(0);
         gridView.setItemAlignmentOffsetPercent(0);
-        ((ViewGroup) view.findViewById(android.support.v17.leanback.R.id.guidedactions_list))
+        ((ViewGroup) view.findViewById(androidx.leanback.R.id.guidedactions_list))
                 .setTransitionGroup(false);
         // Needed for the shared element transition.
         // content_frame is defined in leanback.
         ViewGroup group =
-                (ViewGroup) view.findViewById(android.support.v17.leanback.R.id.content_frame);
+                (ViewGroup) view.findViewById(androidx.leanback.R.id.content_frame);
         group.setClipChildren(false);
         group.setClipToPadding(false);
         return view;
diff --git a/common/src/com/android/tv/common/ui/setup/SetupMultiPaneFragment.java b/common/src/com/android/tv/common/ui/setup/SetupMultiPaneFragment.java
index c02d3f5..ee00e9f 100644
--- a/common/src/com/android/tv/common/ui/setup/SetupMultiPaneFragment.java
+++ b/common/src/com/android/tv/common/ui/setup/SetupMultiPaneFragment.java
@@ -112,15 +112,15 @@
     @Override
     protected int[] getParentIdsForDelay() {
         return new int[] {
-            android.support.v17.leanback.R.id.content_fragment,
-            android.support.v17.leanback.R.id.guidedactions_list
+            androidx.leanback.R.id.content_fragment,
+            androidx.leanback.R.id.guidedactions_list
         };
     }
 
     @Override
     public int[] getSharedElementIds() {
         return new int[] {
-            android.support.v17.leanback.R.id.action_fragment_background, R.id.done_button_container
+            androidx.leanback.R.id.action_fragment_background, R.id.done_button_container
         };
     }
 }
diff --git a/common/src/com/android/tv/common/ui/setup/animation/TranslationAnimationCreator.java b/common/src/com/android/tv/common/ui/setup/animation/TranslationAnimationCreator.java
index 13b89ea..5970693 100644
--- a/common/src/com/android/tv/common/ui/setup/animation/TranslationAnimationCreator.java
+++ b/common/src/com/android/tv/common/ui/setup/animation/TranslationAnimationCreator.java
@@ -20,7 +20,7 @@
 import android.animation.ObjectAnimator;
 import android.animation.TimeInterpolator;
 import android.graphics.Path;
-import android.support.v17.leanback.R;
+import androidx.leanback.R;
 import android.transition.Transition;
 import android.transition.TransitionValues;
 import android.view.View;
@@ -29,9 +29,9 @@
  * This class is used by Slide and Explode to create an animator that goes from the start position
  * to the end position. It takes into account the canceled position so that it will not blink out or
  * shift suddenly when the transition is interrupted. The original class is
- * android.support.v17.leanback.transition.TranslationAnimationCreator which is hidden.
+ * androidx.leanback.transition.TranslationAnimationCreator which is hidden.
  */
-// Copied from android.support.v17.leanback.transition.TransltaionAnimationCreator
+// Copied from androidx.leanback.transition.TransltaionAnimationCreator
 class TranslationAnimationCreator {
     /**
      * Creates an animator that can be used for x and/or y translations. When interrupted, it sets a
diff --git a/common/src/com/android/tv/common/util/CommonUtils.java b/common/src/com/android/tv/common/util/CommonUtils.java
index 4513a87..662f819 100644
--- a/common/src/com/android/tv/common/util/CommonUtils.java
+++ b/common/src/com/android/tv/common/util/CommonUtils.java
@@ -22,10 +22,8 @@
 import android.os.Build;
 import android.util.ArraySet;
 import android.util.Log;
-import com.android.tv.common.BuildConfig;
 import com.android.tv.common.CommonConstants;
 import com.android.tv.common.actions.InputSetupActionUtils;
-import com.android.tv.common.experiments.Experiments;
 import java.io.File;
 import java.text.SimpleDateFormat;
 import java.util.Date;
@@ -53,6 +51,7 @@
 
     static {
         BUNDLED_PACKAGE_SET.add("com.android.tv");
+// AOSP_Comment_Out         BUNDLED_PACKAGE_SET.add(CommonConstants.BASE_PACKAGE);
     }
 
     private static Boolean sRunningInTest;
@@ -123,16 +122,11 @@
         return false;
     }
 
-    /** Returns true if the application is packaged with Live TV. */
+    /** Returns true if the application is packaged with TV app. */
     public static boolean isPackagedWithLiveChannels(Context context) {
         return (CommonConstants.BASE_PACKAGE.equals(context.getPackageName()));
     }
 
-    /** Returns true if the current user is a developer. */
-    public static boolean isDeveloper() {
-        return BuildConfig.ENG || Experiments.ENABLE_DEVELOPER_FEATURES.get();
-    }
-
     /** Converts time in milliseconds to a ISO 8061 string. */
     public static String toIsoDateTimeString(long timeMillis) {
         return ISO_8601.get().format(new Date(timeMillis));
diff --git a/common/src/com/android/tv/common/util/Debug.java b/common/src/com/android/tv/common/util/Debug.java
index ab90874..8e826ae 100644
--- a/common/src/com/android/tv/common/util/Debug.java
+++ b/common/src/com/android/tv/common/util/Debug.java
@@ -23,11 +23,11 @@
 /** A class only for help developers. */
 public class Debug {
     /**
-     * A threshold of start up time, when the start up time of Live TV is more than it, a
-     * warning will show to the developer.
+     * A threshold of start up time, when the start up time of TV app is more than it, a warning
+     * will show to the developer.
      */
     public static final long TIME_START_UP_DURATION_THRESHOLD = TimeUnit.SECONDS.toMillis(6);
-    /** Tag for measuring start up time of Live TV. */
+    /** Tag for measuring start up time of TV app. */
     public static final String TAG_START_UP_TIMER = "start_up_timer";
 
     /** A global map for duration timers. */
diff --git a/common/src/com/android/tv/common/util/LocationUtils.java b/common/src/com/android/tv/common/util/LocationUtils.java
index ee5119e..9d44cf2 100644
--- a/common/src/com/android/tv/common/util/LocationUtils.java
+++ b/common/src/com/android/tv/common/util/LocationUtils.java
@@ -16,9 +16,7 @@
 
 package com.android.tv.common.util;
 
-import android.Manifest;
 import android.content.Context;
-import android.content.pm.PackageManager;
 import android.location.Address;
 import android.location.Geocoder;
 import android.location.Location;
@@ -26,13 +24,12 @@
 import android.location.LocationManager;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.text.TextUtils;
 import android.util.Log;
+
 import com.android.tv.common.BuildConfig;
 
-
-
-
 import java.io.IOException;
 import java.util.Collections;
 import java.util.HashSet;
@@ -65,19 +62,41 @@
         if (sApplicationContext == null) {
             sApplicationContext = context.getApplicationContext();
         }
+        /* Begin_AOSP_Comment_Out
+        if (!BuildConfig.AOSP) {
+            com.google.android.tv.livechannels.util.GoogleLocationUtilsHelper.startLocationUpdates(
+                    context, LocationUtils::updateAddress);
+            return null;
+        }
+        End_AOSP_Comment_Out */
         LocationUtilsHelper.startLocationUpdates();
         return null;
     }
 
+    @Nullable
+    static String getCurrentPostalCode(Context context) throws IOException {
+        Address address = getCurrentAddress(context);
+        if (address != null) {
+            Log.i(
+                    TAG,
+                    "Current country and postal code is "
+                            + address.getCountryName()
+                            + ", "
+                            + address.getPostalCode());
+            return address.getPostalCode();
+        }
+        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.
+         * <p>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.
+         *     {@code false} otherwise.
          */
         boolean onUpdateAddress(Address address);
     }
@@ -85,8 +104,8 @@
     /**
      * Add an {@link OnUpdateAddressListener} instance.
      *
-     * Note that the listener is removed automatically when
-     * {@link OnUpdateAddressListener#onUpdateAddress(Address)} is called and returns {@code true}.
+     * <p>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);
@@ -95,8 +114,8 @@
     /**
      * 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}.
+     * <p>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);
@@ -108,6 +127,13 @@
         if (sCountry != null) {
             return sCountry;
         }
+        /* Begin_AOSP_Comment_Out
+        if (!BuildConfig.AOSP) {
+            sCountry =
+                    com.google.android.tv.livechannels.util.GoogleLocationUtilsHelper
+                            .getDeviceCountry(context);
+        }
+        End_AOSP_Comment_Out */
         if (TextUtils.isEmpty(sCountry)) {
             sCountry = context.getResources().getConfiguration().locale.getCountry();
         }
diff --git a/common/src/com/android/tv/common/util/NetworkTrafficTags.java b/common/src/com/android/tv/common/util/NetworkTrafficTags.java
index 3c94aed..51b6c4d 100644
--- a/common/src/com/android/tv/common/util/NetworkTrafficTags.java
+++ b/common/src/com/android/tv/common/util/NetworkTrafficTags.java
@@ -20,7 +20,7 @@
 import android.support.annotation.NonNull;
 import java.util.concurrent.Executor;
 
-/** Constants for tagging network traffic in the Live channels app. */
+/** Constants for tagging network traffic in the TV app. */
 public final class NetworkTrafficTags {
 
     public static final int DEFAULT_LIVE_CHANNELS = 1;
@@ -43,16 +43,16 @@
 
         @Override
         public void execute(final @NonNull Runnable command) {
-      // TODO(b/62038127): robolectric does not support lamdas in unbundled apps
-      delegateExecutor.execute(
-          () -> {
-            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 ca1abdc..e241b91 100644
--- a/common/src/com/android/tv/common/util/PermissionUtils.java
+++ b/common/src/com/android/tv/common/util/PermissionUtils.java
@@ -26,6 +26,9 @@
     private static Boolean sHasAccessAllEpgPermission;
     private static Boolean sHasAccessWatchedHistoryPermission;
     private static Boolean sHasModifyParentalControlsPermission;
+    private static Boolean sHasChangeHdmiCecActiveSource;
+    private static Boolean sHasReadContentRatingSystem;
+
 
     public static boolean hasAccessAllEpg(Context context) {
         if (sHasAccessAllEpgPermission == null) {
@@ -70,4 +73,24 @@
         return context.checkSelfPermission("android.permission.WRITE_EXTERNAL_STORAGE")
                 == PackageManager.PERMISSION_GRANTED;
     }
+
+    public static boolean hasChangeHdmiCecActiveSource(Context context) {
+        if (sHasChangeHdmiCecActiveSource == null) {
+            sHasChangeHdmiCecActiveSource =
+                    context.checkSelfPermission(
+                            "android.permission.CHANGE_HDMI_CEC_ACTIVE_SOURCE")
+                            == PackageManager.PERMISSION_GRANTED;
+        }
+        return sHasChangeHdmiCecActiveSource;
+    }
+
+    public static boolean hasReadContetnRatingSystem(Context context) {
+        if (sHasReadContentRatingSystem == null) {
+            sHasReadContentRatingSystem =
+                    context.checkSelfPermission(
+                            "android.permission.READ_CONTENT_RATING_SYSTEMS")
+                            == PackageManager.PERMISSION_GRANTED;
+        }
+        return sHasReadContentRatingSystem;
+    }
 }
diff --git a/common/src/com/android/tv/common/util/PostalCodeUtils.java b/common/src/com/android/tv/common/util/PostalCodeUtils.java
index c0917af..6ca3d48 100644
--- a/common/src/com/android/tv/common/util/PostalCodeUtils.java
+++ b/common/src/com/android/tv/common/util/PostalCodeUtils.java
@@ -17,12 +17,12 @@
 package com.android.tv.common.util;
 
 import android.content.Context;
-import android.location.Address;
 import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
 import android.text.TextUtils;
 import android.util.Log;
+
 import com.android.tv.common.CommonPreferences;
+
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.Locale;
@@ -62,7 +62,7 @@
     /** Returns {@code true} if postal code has been changed */
     public static boolean updatePostalCode(Context context)
             throws IOException, SecurityException, NoPostalCodeException {
-        String postalCode = getPostalCode(context);
+        String postalCode = LocationUtils.getCurrentPostalCode(context);
         String lastPostalCode = getLastPostalCode(context);
         if (TextUtils.isEmpty(postalCode)) {
             if (TextUtils.isEmpty(lastPostalCode)) {
@@ -92,21 +92,6 @@
         CommonPreferences.setLastPostalCode(context, postalCode);
     }
 
-    @Nullable
-    private static String getPostalCode(Context context) throws IOException, SecurityException {
-        Address address = LocationUtils.getCurrentAddress(context);
-        if (address != null) {
-            Log.i(
-                    TAG,
-                    "Current country and postal code is "
-                            + address.getCountryName()
-                            + ", "
-                            + address.getPostalCode());
-            return address.getPostalCode();
-        }
-        return null;
-    }
-
     /** An {@link java.lang.Exception} class to notify no valid postal or zip code is available. */
     public static class NoPostalCodeException extends Exception {
         public NoPostalCodeException() {}
diff --git a/common/src/com/android/tv/common/util/SystemProperties.java b/common/src/com/android/tv/common/util/SystemProperties.java
index 6ac2907..72920b6 100644
--- a/common/src/com/android/tv/common/util/SystemProperties.java
+++ b/common/src/com/android/tv/common/util/SystemProperties.java
@@ -21,25 +21,6 @@
 /** A convenience class for getting TV related system properties. */
 public final class SystemProperties {
 
-    /** Allow Google Analytics for eng builds. */
-    public static final BooleanSystemProperty ALLOW_ANALYTICS_IN_ENG =
-            new BooleanSystemProperty("tv_allow_analytics_in_eng", false);
-
-    /** Allow Strict mode for debug builds. */
-    public static final BooleanSystemProperty ALLOW_STRICT_MODE =
-            new BooleanSystemProperty("tv_allow_strict_mode", true);
-
-    /** When true {@link android.view.KeyEvent}s are logged. Defaults to false. */
-    public static final BooleanSystemProperty LOG_KEYEVENT =
-            new BooleanSystemProperty("tv_log_keyevent", false);
-    /** When true debug keys are used. Defaults to false. */
-    public static final BooleanSystemProperty USE_DEBUG_KEYS =
-            new BooleanSystemProperty("tv_use_debug_keys", false);
-
-    /** Send {@link com.android.tv.analytics.Tracker} information. Defaults to {@code true}. */
-    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);
diff --git a/src/com/android/tv/util/SqlParams.java b/common/src/com/android/tv/common/util/sql/SqlParams.java
similarity index 97%
rename from src/com/android/tv/util/SqlParams.java
rename to common/src/com/android/tv/common/util/sql/SqlParams.java
index fa557ba..87fcabd 100644
--- a/src/com/android/tv/util/SqlParams.java
+++ b/common/src/com/android/tv/common/util/sql/SqlParams.java
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.tv.util;
+package com.android.tv.common.util.sql;
 
 import android.database.DatabaseUtils;
 import android.support.annotation.Nullable;
diff --git a/common/tests/robotests/src/com/android/tv/common/TvContentRatingCacheTest.java b/common/tests/robotests/src/com/android/tv/common/TvContentRatingCacheTest.java
new file mode 100644
index 0000000..01bfdc4
--- /dev/null
+++ b/common/tests/robotests/src/com/android/tv/common/TvContentRatingCacheTest.java
@@ -0,0 +1,219 @@
+/*
+ * 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.common;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.content.ComponentCallbacks2;
+import android.media.tv.TvContentRating;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.android.tv.testing.constants.TvContentRatingConstants;
+import com.google.common.collect.ImmutableList;
+import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+/** Test for {@link TvContentRatingCache}. */
+@RunWith(GoogleRobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class TvContentRatingCacheTest {
+    /** US_TV_MA and US_TV_Y7 in order */
+    public static final String MA_AND_Y7 =
+            TvContentRatingConstants.STRING_US_TV_MA
+                    + ","
+                    + TvContentRatingConstants.STRING_US_TV_Y7_US_TV_FV;
+
+    /** US_TV_MA and US_TV_Y7 not in order */
+    public static final String Y7_AND_MA =
+            TvContentRatingConstants.STRING_US_TV_Y7_US_TV_FV
+                    + ","
+                    + TvContentRatingConstants.STRING_US_TV_MA;
+
+    final TvContentRatingCache mCache = TvContentRatingCache.getInstance();
+
+    @Before
+    public void setUp() {
+        mCache.performTrimMemory(ComponentCallbacks2.TRIM_MEMORY_COMPLETE);
+    }
+
+    @After
+    public void tearDown() {
+        mCache.performTrimMemory(ComponentCallbacks2.TRIM_MEMORY_COMPLETE);
+    }
+
+    @Test
+    public void testGetRatings_US_TV_MA() {
+        ImmutableList<TvContentRating> result =
+                mCache.getRatings(TvContentRatingConstants.STRING_US_TV_MA);
+        assertThat(result).contains(TvContentRatingConstants.CONTENT_RATING_US_TV_MA);
+    }
+
+    @Test
+    public void testGetRatings_US_TV_MA_same() {
+        ImmutableList<TvContentRating> first =
+                mCache.getRatings(TvContentRatingConstants.STRING_US_TV_MA);
+        ImmutableList<TvContentRating> second =
+                mCache.getRatings(TvContentRatingConstants.STRING_US_TV_MA);
+    assertThat(first).isSameInstanceAs(second);
+    }
+
+    @Test
+    public void testGetRatings_US_TV_MA_diffAfterClear() {
+        ImmutableList<TvContentRating> first =
+                mCache.getRatings(TvContentRatingConstants.STRING_US_TV_MA);
+        mCache.performTrimMemory(ComponentCallbacks2.TRIM_MEMORY_COMPLETE);
+        ImmutableList<TvContentRating> second =
+                mCache.getRatings(TvContentRatingConstants.STRING_US_TV_MA);
+    assertThat(first).isNotSameInstanceAs(second);
+    }
+
+    @Test
+    public void testGetRatings_TWO_orderDoesNotMatter() {
+        ImmutableList<TvContentRating> first = mCache.getRatings(MA_AND_Y7);
+        ImmutableList<TvContentRating> second = mCache.getRatings(Y7_AND_MA);
+    assertThat(first).isSameInstanceAs(second);
+    }
+
+    @Test
+    public void testContentRatingsToString_null() {
+        String result = TvContentRatingCache.contentRatingsToString(null);
+    assertWithMessage("ratings string").that(result).isNull();
+    }
+
+    @Test
+    public void testContentRatingsToString_none() {
+        String result = TvContentRatingCache.contentRatingsToString(ImmutableList.of());
+    assertWithMessage("ratings string").that(result).isEmpty();
+    }
+
+    @Test
+    public void testContentRatingsToString_one() {
+        String result =
+                TvContentRatingCache.contentRatingsToString(
+                        ImmutableList.of(TvContentRatingConstants.CONTENT_RATING_US_TV_MA));
+    assertWithMessage("ratings string")
+        .that(result)
+        .isEqualTo(TvContentRatingConstants.STRING_US_TV_MA);
+    }
+
+    @Test
+    public void testContentRatingsToString_twoInOrder() {
+        String result =
+                TvContentRatingCache.contentRatingsToString(
+                        ImmutableList.of(
+                                TvContentRatingConstants.CONTENT_RATING_US_TV_MA,
+                                TvContentRatingConstants.CONTENT_RATING_US_TV_Y7_US_TV_FV));
+    assertWithMessage("ratings string").that(result).isEqualTo(MA_AND_Y7);
+    }
+
+    @Test
+    public void testContentRatingsToString_twoNotInOrder() {
+        String result =
+                TvContentRatingCache.contentRatingsToString(
+                        ImmutableList.of(
+                                TvContentRatingConstants.CONTENT_RATING_US_TV_Y7_US_TV_FV,
+                                TvContentRatingConstants.CONTENT_RATING_US_TV_MA));
+    assertWithMessage("ratings string").that(result).isEqualTo(MA_AND_Y7);
+    }
+
+    @Test
+    public void testContentRatingsToString_double() {
+        String result =
+                TvContentRatingCache.contentRatingsToString(
+                        ImmutableList.of(
+                                TvContentRatingConstants.CONTENT_RATING_US_TV_MA,
+                                TvContentRatingConstants.CONTENT_RATING_US_TV_MA));
+    assertWithMessage("ratings string")
+        .that(result)
+        .isEqualTo(TvContentRatingConstants.STRING_US_TV_MA);
+    }
+
+    @Test
+    public void testStringToContentRatings_null() {
+        assertThat(TvContentRatingCache.stringToContentRatings(null)).isEmpty();
+    }
+
+    @Test
+    public void testStringToContentRatings_none() {
+        assertThat(TvContentRatingCache.stringToContentRatings("")).isEmpty();
+    }
+
+    @Test
+    public void testStringToContentRatings_bad() {
+        assertThat(TvContentRatingCache.stringToContentRatings("bad")).isEmpty();
+    }
+
+    @Test
+    public void testStringToContentRatings_oneGoodOneBad() {
+        ImmutableList<TvContentRating> results =
+                TvContentRatingCache.stringToContentRatings(
+                        TvContentRatingConstants.STRING_US_TV_Y7_US_TV_FV + ",bad");
+    assertWithMessage("ratings")
+        .that(results)
+        .containsExactly(TvContentRatingConstants.CONTENT_RATING_US_TV_Y7_US_TV_FV);
+    }
+
+    @Test
+    public void testStringToContentRatings_one() {
+        ImmutableList<TvContentRating> results =
+                TvContentRatingCache.stringToContentRatings(
+                        TvContentRatingConstants.STRING_US_TV_Y7_US_TV_FV);
+    assertWithMessage("ratings")
+        .that(results)
+        .containsExactly(TvContentRatingConstants.CONTENT_RATING_US_TV_Y7_US_TV_FV);
+    }
+
+    @Test
+    public void testStringToContentRatings_twoNotInOrder() {
+        ImmutableList<TvContentRating> results =
+                TvContentRatingCache.stringToContentRatings(Y7_AND_MA);
+    assertWithMessage("ratings")
+        .that(results)
+        .containsExactly(
+            TvContentRatingConstants.CONTENT_RATING_US_TV_MA,
+            TvContentRatingConstants.CONTENT_RATING_US_TV_Y7_US_TV_FV);
+    }
+
+    @Test
+    public void testStringToContentRatings_twoInOrder() {
+        ImmutableList<TvContentRating> results =
+                TvContentRatingCache.stringToContentRatings(MA_AND_Y7);
+    assertWithMessage("ratings")
+        .that(results)
+        .containsExactly(
+            TvContentRatingConstants.CONTENT_RATING_US_TV_MA,
+            TvContentRatingConstants.CONTENT_RATING_US_TV_Y7_US_TV_FV);
+    }
+
+    @Test
+    public void testStringToContentRatings_double() {
+        ImmutableList<TvContentRating> results =
+                TvContentRatingCache.stringToContentRatings(
+                        TvContentRatingConstants.STRING_US_TV_MA
+                                + ","
+                                + TvContentRatingConstants.STRING_US_TV_MA);
+    assertWithMessage("ratings")
+        .that(results)
+        .containsExactly((TvContentRatingConstants.CONTENT_RATING_US_TV_MA));
+
+        assertThat(results).containsExactly(TvContentRatingConstants.CONTENT_RATING_US_TV_MA);
+    }
+}
diff --git a/common/tests/robotests/src/com/android/tv/common/actions/InputSetupActionUtilsTest.java b/common/tests/robotests/src/com/android/tv/common/actions/InputSetupActionUtilsTest.java
new file mode 100644
index 0000000..17d400a
--- /dev/null
+++ b/common/tests/robotests/src/com/android/tv/common/actions/InputSetupActionUtilsTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.actions;
+
+import static com.google.android.libraries.testing.truth.BundleSubject.assertThat;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Intent;
+import android.os.Bundle;
+import com.android.tv.testing.constants.ConfigConstants;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link InputSetupActionUtils} */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class InputSetupActionUtilsTest {
+
+    @Test
+    public void hasInputSetupAction_launchInputSetup() {
+        Intent intent = new Intent("com.android.tv.action.LAUNCH_INPUT_SETUP");
+        assertThat(InputSetupActionUtils.hasInputSetupAction(intent)).isTrue();
+    }
+
+    @Test
+    public void hasInputSetupAction_googleLaunchInputSetup() {
+        Intent intent = new Intent("com.google.android.tv.action.LAUNCH_INPUT_SETUP");
+        assertThat(InputSetupActionUtils.hasInputSetupAction(intent)).isTrue();
+    }
+
+    @Test
+    public void hasInputSetupAction_bad() {
+        Intent intent = new Intent("com.example.action.LAUNCH_INPUT_SETUP");
+        assertThat(InputSetupActionUtils.hasInputSetupAction(intent)).isFalse();
+    }
+
+    @Test
+    public void getExtraActivityAfter_null() {
+        Intent intent = new Intent();
+        assertThat(InputSetupActionUtils.getExtraActivityAfter(intent)).isNull();
+    }
+
+    @Test
+    public void getExtraActivityAfter_activityAfter() {
+        Intent intent = new Intent();
+        Intent after = new Intent("after");
+        intent.putExtra("com.android.tv.intent.extra.ACTIVITY_AFTER_COMPLETION", after);
+        assertThat(InputSetupActionUtils.getExtraActivityAfter(intent)).isEqualTo(after);
+    }
+
+    @Test
+    public void getExtraActivityAfter_googleActivityAfter() {
+        Intent intent = new Intent();
+        Intent after = new Intent("google_setup");
+        intent.putExtra("com.google.android.tv.intent.extra.ACTIVITY_AFTER_COMPLETION", after);
+        assertThat(InputSetupActionUtils.getExtraActivityAfter(intent)).isEqualTo(after);
+    }
+
+    @Test
+    public void getExtraSetupIntent_null() {
+        Intent intent = new Intent();
+        assertThat(InputSetupActionUtils.getExtraSetupIntent(intent)).isNull();
+    }
+
+    @Test
+    public void getExtraSetupIntent_setupIntent() {
+        Intent intent = new Intent();
+        Intent setup = new Intent("setup");
+        intent.putExtra("com.android.tv.extra.SETUP_INTENT", setup);
+        assertThat(InputSetupActionUtils.getExtraSetupIntent(intent)).isEqualTo(setup);
+    }
+
+    @Test
+    public void getExtraSetupIntent_googleSetupIntent() {
+        Intent intent = new Intent();
+        Intent setup = new Intent("google_setup");
+        intent.putExtra("com.google.android.tv.extra.SETUP_INTENT", setup);
+        assertThat(InputSetupActionUtils.getExtraSetupIntent(intent)).isEqualTo(setup);
+    }
+
+    @Test
+    public void removeSetupIntent_empty() {
+        Bundle extras = new Bundle();
+        InputSetupActionUtils.removeSetupIntent(extras);
+        assertThat(extras).exactlyMatches(new Bundle());
+    }
+
+    @Test
+    public void removeSetupIntent_other() {
+        Bundle extras = createTestBundle();
+        Bundle expected = createTestBundle();
+        InputSetupActionUtils.removeSetupIntent(extras);
+        assertThat(extras).exactlyMatches(expected);
+    }
+
+    @Test
+    public void removeSetupIntent_setup() {
+        Bundle extras = createTestBundle();
+        Bundle expected = createTestBundle();
+        Intent setup = new Intent("setup");
+        extras.putParcelable("com.android.tv.extra.SETUP_INTENT", setup);
+        InputSetupActionUtils.removeSetupIntent(extras);
+        assertThat(extras).exactlyMatches(expected);
+    }
+
+    @Test
+    public void removeSetupIntent_googleSetup() {
+        Bundle extras = createTestBundle();
+        Bundle expected = createTestBundle();
+        Intent googleSetup = new Intent("googleSetup");
+        extras.putParcelable("com.google.android.tv.extra.SETUP_INTENT", googleSetup);
+        InputSetupActionUtils.removeSetupIntent(extras);
+        assertThat(extras).exactlyMatches(expected);
+    }
+
+    @Test
+    public void removeSetupIntent_bothSetups() {
+        Bundle extras = createTestBundle();
+        Bundle expected = createTestBundle();
+        Intent setup = new Intent("setup");
+        extras.putParcelable("com.android.tv.extra.SETUP_INTENT", setup);
+        Intent googleSetup = new Intent("googleSetup");
+        extras.putParcelable("com.google.android.tv.extra.SETUP_INTENT", googleSetup);
+        InputSetupActionUtils.removeSetupIntent(extras);
+        assertThat(extras).exactlyMatches(expected);
+    }
+
+    private static Bundle createTestBundle() {
+        Bundle extras = new Bundle();
+        extras.putInt("other", 1);
+        return extras;
+    }
+}
diff --git a/common/tests/robotests/src/com/android/tv/common/compat/TvInputInfoCompatTest.java b/common/tests/robotests/src/com/android/tv/common/compat/TvInputInfoCompatTest.java
new file mode 100644
index 0000000..93c04b9
--- /dev/null
+++ b/common/tests/robotests/src/com/android/tv/common/compat/TvInputInfoCompatTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+import static junit.framework.Assert.fail;
+
+import android.content.pm.ResolveInfo;
+import android.content.res.XmlResourceParser;
+import android.media.tv.TvInputInfo;
+import androidx.test.InstrumentationRegistry;
+
+import com.android.tv.testing.constants.ConfigConstants;
+import com.android.tv.testing.utils.TestUtils;
+
+import java.io.StringReader;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+/**
+ * Tests for {@link TvInputInfoCompat}.
+ */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class TvInputInfoCompatTest {
+    private TvInputInfoCompat mTvInputInfoCompat;
+    private String mInputXml;
+    @Before
+    public void setUp() throws Exception {
+        ResolveInfo resolveInfo = TestUtils.createResolveInfo("test", "test");
+        TvInputInfo info =
+                TestUtils.createTvInputInfo(
+                        resolveInfo, "test_input", "test1", TvInputInfo.TYPE_OTHER, false);
+        mTvInputInfoCompat =
+                new TvInputInfoCompat(
+                        InstrumentationRegistry.getTargetContext(), info) {
+                    @Override
+                    XmlPullParser getXmlResourceParser() {
+                        XmlPullParser xpp = null;
+                        try {
+                            xpp = XmlPullParserFactory.newInstance().newPullParser();
+                            xpp.setInput(new StringReader(mInputXml));
+                            xpp.setFeature(
+                                    XmlResourceParser.FEATURE_PROCESS_NAMESPACES, true);
+                        } catch (XmlPullParserException e) {
+                            fail("failed in setUp() " + e.getMessage());
+                        }
+                        return xpp;
+                    }
+                };
+    }
+
+    @Test
+    public void testGetAttributeValue_notTvInputTag() {
+        mInputXml =
+                "<not-tv-input xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"
+                        + "    android:setupActivity=\"\"\n"
+                        + "    android:settingsActivity=\"\"/>\n";
+        assertThat(mTvInputInfoCompat.getExtras()).isEmpty();
+    }
+
+    @Test
+    public void testGetAttributeValue_noExtra() {
+        mInputXml =
+                "<not-tv-input xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"
+                        + "    android:setupActivity=\"\"\n"
+                        + "    android:settingsActivity=\"\"/>\n";
+        assertThat(mTvInputInfoCompat.getExtras()).isEmpty();
+    }
+
+    @Test
+    public void testGetAttributeValue() {
+        mInputXml =
+                "<tv-input xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"
+                        + "    android:setupActivity=\"\"\n"
+                        + "    android:settingsActivity=\"\">\n"
+                        + "      <extra android:name=\"otherAttr1\" android:value=\"false\" />\n"
+                        + "      <extra android:name=\"otherAttr2\" android:value=\"false\" />\n"
+                        + "      <extra android:name="
+                        + "          \"com.android.tv.common.compat.tvinputinfocompat.audioOnly\""
+                        + "          android:value=\"true\" />\n"
+                        + "</tv-input>";
+        assertThat(mTvInputInfoCompat.getExtras())
+                .containsExactly("otherAttr1", "false", "otherAttr2", "false",
+                        "com.android.tv.common.compat.tvinputinfocompat.audioOnly", "true");
+    }
+}
diff --git a/common/tests/robotests/src/com/android/tv/common/compat/internal/PrivateCommandTest.java b/common/tests/robotests/src/com/android/tv/common/compat/internal/PrivateCommandTest.java
new file mode 100644
index 0000000..02705a4
--- /dev/null
+++ b/common/tests/robotests/src/com/android/tv/common/compat/internal/PrivateCommandTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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 static org.mockito.Mockito.only;
+import static org.mockito.Mockito.verify;
+
+import com.android.tv.common.compat.api.SessionCompatCommands;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.testing.mockito.Mocks;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/**
+ * Tests sending {@link Commands.PrivateCommand}s to a {@link SessionCompatCommands} from {@link
+ * TvViewCompatProcessor} via {@link TifSessionCompatProcessor}.
+ */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class PrivateCommandTest {
+    @Rule public final Mocks mocks = new Mocks(this);
+
+    @Mock SessionCompatCommands mCallback;
+
+    private TvViewCompatProcessor mTvViewCompatProcessor;
+
+    @Before
+    public void setUp() {
+        TifSessionCompatProcessor sessionCompatProcessor =
+                new TifSessionCompatProcessor(null, mCallback);
+        mTvViewCompatProcessor =
+                new TvViewCompatProcessor(sessionCompatProcessor::handleAppPrivateCommand);
+    }
+
+    @Test
+    public void notifyDevToast() throws InvalidProtocolBufferException {
+        mTvViewCompatProcessor.devMessage("Hello Developers");
+        verify(mCallback, only()).onDevMessage("Hello Developers");
+    }
+}
diff --git a/common/tests/robotests/src/com/android/tv/common/compat/internal/PrivateRecordingCommandTest.java b/common/tests/robotests/src/com/android/tv/common/compat/internal/PrivateRecordingCommandTest.java
new file mode 100644
index 0000000..6d9949f
--- /dev/null
+++ b/common/tests/robotests/src/com/android/tv/common/compat/internal/PrivateRecordingCommandTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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 static org.mockito.Mockito.only;
+import static org.mockito.Mockito.verify;
+
+import com.android.tv.common.compat.api.RecordingSessionCompatCommands;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.testing.mockito.Mocks;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/**
+ * Tests sending {@link RecordingCommands.PrivateRecordingCommand}s to a {@link
+ * RecordingSessionCompatCommands} from {@link RecordingClientCompatProcessor} via {@link
+ * RecordingSessionCompatProcessor}.
+ */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class PrivateRecordingCommandTest {
+    @Rule public final Mocks mocks = new Mocks(this);
+
+    @Mock private RecordingSessionCompatCommands mCallback;
+
+    private RecordingClientCompatProcessor mCompatProcessor;
+
+    @Before
+    public void setUp() {
+        RecordingSessionCompatProcessor sessionCompatProcessor =
+                new RecordingSessionCompatProcessor(null, mCallback);
+        mCompatProcessor =
+                new RecordingClientCompatProcessor(
+                        sessionCompatProcessor::handleAppPrivateCommand, null);
+    }
+
+    @Test
+    public void notifyDevToast() throws InvalidProtocolBufferException {
+        mCompatProcessor.devMessage("Hello Recorders");
+        verify(mCallback, only()).onDevMessage("Hello Recorders");
+    }
+}
diff --git a/common/tests/robotests/src/com/android/tv/common/compat/internal/RecordingSessionEventTest.java b/common/tests/robotests/src/com/android/tv/common/compat/internal/RecordingSessionEventTest.java
new file mode 100644
index 0000000..b5bd0a0
--- /dev/null
+++ b/common/tests/robotests/src/com/android/tv/common/compat/internal/RecordingSessionEventTest.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.common.compat.internal;
+
+import static org.mockito.Mockito.only;
+import static org.mockito.Mockito.verify;
+
+import com.android.tv.common.compat.api.RecordingClientCallbackCompatEvents;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.testing.mockito.Mocks;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/**
+ * Tests sending {@link RecordingEvents.RecordingSessionEvent}s to a {@link
+ * RecordingClientCallbackCompatEvents} from {@link RecordingSessionCompatProcessor} via {@link
+ * RecordingClientCompatProcessor}.
+ */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class RecordingSessionEventTest {
+    @Rule public final Mocks mocks = new Mocks(this);
+
+    @Mock RecordingClientCallbackCompatEvents mCallback;
+
+    private RecordingSessionCompatProcessor mCompatProcess;
+
+    @Before
+    public void setUp() {
+        RecordingClientCompatProcessor compatProcessor =
+                new RecordingClientCompatProcessor(null, mCallback);
+        mCompatProcess =
+                new RecordingSessionCompatProcessor(
+                        (event, data) -> compatProcessor.handleEvent("testinput", event, data),
+                        null);
+    }
+
+    @Test
+    public void notifyDevToast() throws InvalidProtocolBufferException {
+        mCompatProcess.notifyDevToast("Recording");
+        verify(mCallback, only()).onDevToast("testinput", "Recording");
+    }
+
+    @Test
+    public void notifyRecordingStarted() throws InvalidProtocolBufferException {
+        mCompatProcess.notifyRecordingStarted("file:example");
+        verify(mCallback, only()).onRecordingStarted("testinput", "file:example");
+    }
+}
diff --git a/common/tests/robotests/src/com/android/tv/common/compat/internal/SessionEventTest.java b/common/tests/robotests/src/com/android/tv/common/compat/internal/SessionEventTest.java
new file mode 100644
index 0000000..0cc300b
--- /dev/null
+++ b/common/tests/robotests/src/com/android/tv/common/compat/internal/SessionEventTest.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.common.compat.internal;
+
+import static org.mockito.Mockito.only;
+import static org.mockito.Mockito.verify;
+
+import com.android.tv.common.compat.api.TvInputCallbackCompatEvents;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.testing.mockito.Mocks;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/**
+ * Tests sending {@link Events.SessionEvent}s to a {@link TvInputCallbackCompatEvents} from {@link
+ * TifSessionCompatProcessor} via {@link TvViewCompatProcessor}.
+ */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class SessionEventTest {
+    @Rule public final Mocks mocks = new Mocks(this);
+
+    @Mock TvInputCallbackCompatEvents mCallback;
+
+    private TifSessionCompatProcessor mCompatProcess;
+
+    @Before
+    public void setUp() {
+        TvViewCompatProcessor tvViewCompatProcessor = new TvViewCompatProcessor(null);
+        tvViewCompatProcessor.setCallback(mCallback);
+        mCompatProcess =
+                new TifSessionCompatProcessor(
+                        (event, data) ->
+                                tvViewCompatProcessor.handleEvent("testinput", event, data),
+                        null);
+    }
+
+    @Test
+    public void notifyDevToast() throws InvalidProtocolBufferException {
+        mCompatProcess.notifyDevToast("testing");
+        verify(mCallback, only()).onDevToast("testinput", "testing");
+    }
+
+    @Test
+    public void notifySignalStrength() throws InvalidProtocolBufferException {
+        mCompatProcess.notifySignalStrength(3);
+        verify(mCallback, only()).onSignalStrength("testinput", 3);
+    }
+}
diff --git a/common/tests/robotests/src/com/android/tv/common/dev/DeveloperPreferenceTest.java b/common/tests/robotests/src/com/android/tv/common/dev/DeveloperPreferenceTest.java
new file mode 100644
index 0000000..a9c15ad
--- /dev/null
+++ b/common/tests/robotests/src/com/android/tv/common/dev/DeveloperPreferenceTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.dev;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.tv.testing.constants.ConfigConstants;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+/** Test for {@link DeveloperPreference}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class DeveloperPreferenceTest {
+
+    @Test
+    public void createBoolean_default_true() {
+        DeveloperPreference<Boolean> devPref = DeveloperPreference.create("test", true);
+        assertThat(devPref.get(RuntimeEnvironment.systemContext)).isTrue();
+        DeveloperPreference.getPreferences(RuntimeEnvironment.systemContext)
+                .edit()
+                .putBoolean("test", false)
+                .apply();
+        assertThat(devPref.get(RuntimeEnvironment.systemContext)).isFalse();
+    }
+
+    @Test
+    public void create_integer_default_one() {
+        DeveloperPreference<Integer> devPref = DeveloperPreference.create("test", 1);
+        assertThat(devPref.get(RuntimeEnvironment.systemContext)).isEqualTo(1);
+        DeveloperPreference.getPreferences(RuntimeEnvironment.systemContext)
+                .edit()
+                .putInt("test", 2)
+                .apply();
+        assertThat(devPref.get(RuntimeEnvironment.systemContext)).isEqualTo(2);
+    }
+}
diff --git a/common/tests/robotests/src/com/android/tv/common/support/tis/BaseTvInputServiceTest.java b/common/tests/robotests/src/com/android/tv/common/support/tis/BaseTvInputServiceTest.java
new file mode 100644
index 0000000..7deef75
--- /dev/null
+++ b/common/tests/robotests/src/com/android/tv/common/support/tis/BaseTvInputServiceTest.java
@@ -0,0 +1,145 @@
+package com.android.tv.common.support.tis;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Intent;
+import android.media.tv.TvContentRating;
+import android.media.tv.TvInputManager;
+import android.net.Uri;
+import android.os.Build;
+import android.support.annotation.Nullable;
+import android.view.Surface;
+import com.android.tv.common.support.tis.TifSession.TifSessionCallbacks;
+import com.android.tv.common.support.tis.TifSession.TifSessionFactory;
+import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.android.controller.ServiceController;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link BaseTvInputService}. */
+@RunWith(GoogleRobolectricTestRunner.class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP, maxSdk = Build.VERSION_CODES.P)
+public class BaseTvInputServiceTest {
+
+    private static class TestTvInputService extends BaseTvInputService {
+
+        private final SessionManager sessionManager = new SimpleSessionManager(1);
+
+        private int parentalControlsChangedCount = 0;
+        private final TifSessionFactory sessionFactory;
+
+        private TestTvInputService() {
+            super();
+            this.sessionFactory =
+                    new TifSessionFactory() {
+                        @Override
+                        public TifSession create(TifSessionCallbacks callbacks, String inputId) {
+                            return new TifSession(callbacks) {
+                                @Override
+                                public boolean onSetSurface(@Nullable Surface surface) {
+                                    return false;
+                                }
+
+                                @Override
+                                public void onSurfaceChanged(int format, int width, int height) {}
+
+                                @Override
+                                public void onSetStreamVolume(float volume) {}
+
+                                @Override
+                                public boolean onTune(Uri channelUri) {
+                                    return false;
+                                }
+
+                                @Override
+                                public void onSetCaptionEnabled(boolean enabled) {}
+
+                                @Override
+                                public void onUnblockContent(TvContentRating unblockedRating) {}
+
+                                @Override
+                                public void onParentalControlsChanged() {
+                                    parentalControlsChangedCount++;
+                                }
+                            };
+                        }
+                    };
+        }
+
+        @Override
+        protected TifSessionFactory getTifSessionFactory() {
+            return sessionFactory;
+        }
+
+        @Override
+        protected SessionManager getSessionManager() {
+            return sessionManager;
+        }
+
+        private int getParentalControlsChangedCount() {
+            return parentalControlsChangedCount;
+        }
+    }
+
+    TestTvInputService tvInputService;
+    ServiceController<TestTvInputService> controller;
+
+    @Before
+    public void setUp() {
+        controller = Robolectric.buildService(TestTvInputService.class);
+        tvInputService = controller.create().get();
+    }
+
+    @Test
+    public void createSession_once() {
+        assertThat(tvInputService.onCreateSession("test")).isNotNull();
+    }
+
+    @Test
+    public void createSession_twice() {
+        WrappedSession first = tvInputService.onCreateSession("test");
+        assertThat(first).isNotNull();
+        WrappedSession second = tvInputService.onCreateSession("test");
+        assertThat(second).isNull();
+    }
+
+    @Test
+    public void createSession_release() {
+        WrappedSession first = tvInputService.onCreateSession("test");
+        assertThat(first).isNotNull();
+        first.onRelease();
+        WrappedSession second = tvInputService.onCreateSession("test");
+        assertThat(second).isNotNull();
+    assertThat(second).isNotSameInstanceAs(first);
+    }
+
+    @Test
+    public void testReceiver_actionEnabledChanged() {
+        tvInputService.getSessionManager().addSession(tvInputService.onCreateSession("test"));
+        tvInputService.broadcastReceiver.onReceive(
+                RuntimeEnvironment.application,
+                new Intent(TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED));
+        assertThat(tvInputService.getParentalControlsChangedCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void testReceiver_actionBlockedChanged() {
+        tvInputService.getSessionManager().addSession(tvInputService.onCreateSession("test"));
+        tvInputService.broadcastReceiver.onReceive(
+                RuntimeEnvironment.application,
+                new Intent(TvInputManager.ACTION_BLOCKED_RATINGS_CHANGED));
+        assertThat(tvInputService.getParentalControlsChangedCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void testReceiver_invalidAction() {
+        tvInputService.getSessionManager().addSession(tvInputService.onCreateSession("test"));
+        tvInputService.broadcastReceiver.onReceive(
+                RuntimeEnvironment.application, new Intent("test"));
+        assertThat(tvInputService.getParentalControlsChangedCount()).isEqualTo(0);
+    }
+}
diff --git a/common/tests/robotests/src/com/android/tv/common/support/tis/SimpleSessionManagerTest.java b/common/tests/robotests/src/com/android/tv/common/support/tis/SimpleSessionManagerTest.java
new file mode 100644
index 0000000..71ff199
--- /dev/null
+++ b/common/tests/robotests/src/com/android/tv/common/support/tis/SimpleSessionManagerTest.java
@@ -0,0 +1,122 @@
+package com.android.tv.common.support.tis;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.media.tv.TvContentRating;
+import android.net.Uri;
+import android.os.Build;
+import android.support.annotation.FloatRange;
+import android.support.annotation.Nullable;
+import android.view.Surface;
+import com.android.tv.common.support.tis.TifSession.TifSessionCallbacks;
+import com.android.tv.common.support.tis.TifSession.TifSessionFactory;
+import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link SimpleSessionManager}. */
+@RunWith(GoogleRobolectricTestRunner.class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP, maxSdk = Build.VERSION_CODES.P)
+public class SimpleSessionManagerTest {
+
+    private SimpleSessionManager sessionManager;
+
+    @Before
+    public void setup() {
+        sessionManager = new SimpleSessionManager(1);
+    }
+
+    @Test
+    public void canCreateSession_none() {
+        assertThat(sessionManager.canCreateNewSession()).isTrue();
+    }
+
+    @Test
+    public void canCreateSession_one() {
+        sessionManager.addSession(createTestSession());
+        assertThat(sessionManager.canCreateNewSession()).isFalse();
+    }
+
+    @Test
+    public void addSession() {
+        assertThat(sessionManager.getSessionCount()).isEqualTo(0);
+        sessionManager.addSession(createTestSession());
+        assertThat(sessionManager.getSessionCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void onRelease() {
+        WrappedSession testSession = createTestSession();
+        sessionManager.addSession(testSession);
+        assertThat(sessionManager.getSessionCount()).isEqualTo(1);
+        testSession.onRelease();
+        assertThat(sessionManager.getSessionCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void onRelease_withUnRegisteredSession() {
+        WrappedSession testSession = createTestSession();
+        sessionManager.addSession(createTestSession());
+        assertThat(sessionManager.getSessionCount()).isEqualTo(1);
+        testSession.onRelease();
+        assertThat(sessionManager.getSessionCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void getSessions() {
+        WrappedSession testSession = createTestSession();
+        sessionManager.addSession(testSession);
+        assertThat(sessionManager.getSessions()).containsExactly(testSession);
+    }
+
+    private WrappedSession createTestSession() {
+        return new WrappedSession(
+                RuntimeEnvironment.application,
+                sessionManager,
+                new TestTifSessionFactory(),
+                "testInputId");
+    }
+
+    private static final class TestTifSessionFactory implements TifSessionFactory {
+
+        @Override
+        public TifSession create(TifSessionCallbacks callbacks, String inputId) {
+            return new TestTifSession(callbacks);
+        }
+    }
+
+    private static final class TestTifSession extends TifSession {
+
+        private TestTifSession(TifSessionCallbacks callbacks) {
+            super(callbacks);
+        }
+
+        @Override
+        public void onRelease() {}
+
+        @Override
+        public boolean onSetSurface(@Nullable Surface surface) {
+            return false;
+        }
+
+        @Override
+        public void onSurfaceChanged(int format, int width, int height) {}
+
+        @Override
+        public void onSetStreamVolume(@FloatRange(from = 0.0, to = 1.0) float volume) {}
+
+        @Override
+        public boolean onTune(Uri channelUri) {
+            return false;
+        }
+
+        @Override
+        public void onSetCaptionEnabled(boolean enabled) {}
+
+        @Override
+        public void onUnblockContent(TvContentRating unblockedRating) {}
+    }
+}
diff --git a/common/tests/robotests/src/com/android/tv/common/support/tis/TisSessionTest.java b/common/tests/robotests/src/com/android/tv/common/support/tis/TisSessionTest.java
new file mode 100644
index 0000000..e324017
--- /dev/null
+++ b/common/tests/robotests/src/com/android/tv/common/support/tis/TisSessionTest.java
@@ -0,0 +1,205 @@
+package com.android.tv.common.support.tis;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.media.tv.TvContentRating;
+import android.media.tv.TvInputManager;
+import android.media.tv.TvTrackInfo;
+import android.net.Uri;
+import android.os.Build;
+import android.support.annotation.FloatRange;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Pair;
+import android.view.Surface;
+import android.view.View;
+import com.android.tv.common.support.tis.TifSession.TifSessionCallbacks;
+import com.google.common.collect.ImmutableList;
+import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link TifSession}. */
+@RunWith(GoogleRobolectricTestRunner.class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP, maxSdk = Build.VERSION_CODES.P)
+public class TisSessionTest {
+
+    private TestSession testSession;
+    private TestCallback testCallback;
+
+    @Before
+    public void setup() {
+        testCallback = new TestCallback();
+        testSession = new TestSession(testCallback);
+    }
+
+    @Test
+    public void notifyChannelReturned() {
+        Uri uri = Uri.parse("http://example.com");
+        testSession.notifyChannelRetuned(uri);
+        assertThat(testCallback.channelUri).isEqualTo(uri);
+    }
+
+    @Test
+    public void notifyTracksChanged() {
+        List<TvTrackInfo> tracks =
+                ImmutableList.of(new TvTrackInfo.Builder(TvTrackInfo.TYPE_VIDEO, "test").build());
+        testSession.notifyTracksChanged(tracks);
+        assertThat(testCallback.tracks).isEqualTo(tracks);
+    }
+
+    @Test
+    public void notifyTrackSelected() {
+        testSession.notifyTrackSelected(TvTrackInfo.TYPE_AUDIO, "audio_test");
+        assertThat(testCallback.trackSelected)
+                .isEqualTo(Pair.create(TvTrackInfo.TYPE_AUDIO, "audio_test"));
+    }
+
+    @Test
+    public void notifyVideoAvailable() {
+        testSession.notifyVideoAvailable();
+        assertThat(testCallback.videoAvailable).isTrue();
+    }
+
+    @Test
+    public void notifyVideoUnavailable() {
+        testSession.notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+        assertThat(testCallback.notifyVideoUnavailableReason)
+                .isEqualTo(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+    }
+
+    @Test
+    public void notifyContentAllowed() {
+        testSession.notifyContentAllowed();
+        assertThat(testCallback.contentAllowed).isTrue();
+    }
+
+    @Test
+    public void notifyContentBlocked() {
+        TvContentRating rating = TvContentRating.createRating("1", "2", "3");
+        testSession.notifyContentBlocked(rating);
+        assertThat(testCallback.blockedContentRating).isEqualTo(rating);
+    }
+
+    @Test
+    public void notifyTimeShiftStatusChanged() {
+        testSession.notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_AVAILABLE);
+        assertThat(testCallback.timeShiftStatus)
+                .isEqualTo(TvInputManager.TIME_SHIFT_STATUS_AVAILABLE);
+    }
+
+    @Test
+    public void testSetOverlayViewEnabled() {
+        testSession.helpTestSetOverlayViewEnabled(true);
+        assertThat(testCallback.overlayViewEnabled).isTrue();
+
+        testSession.helpTestSetOverlayViewEnabled(false);
+        assertThat(testCallback.overlayViewEnabled).isFalse();
+    }
+
+    @Test
+    public void testOnCreateOverlayView() {
+        View actualView = testSession.onCreateOverlayView();
+        assertThat(actualView).isNull(); // Default implementation returns a null.
+    }
+
+    @Test
+    public void testOnOverlayViewSizeChanged() {
+        testSession.onOverlayViewSizeChanged(5 /* width */, 7 /* height */);
+        // Just verifing that the call completes.
+    }
+
+    private static final class TestCallback implements TifSessionCallbacks {
+
+        private Uri channelUri;
+        private List<TvTrackInfo> tracks;
+        private Pair<Integer, String> trackSelected;
+        private boolean videoAvailable;
+        private int notifyVideoUnavailableReason;
+        private boolean contentAllowed;
+        private TvContentRating blockedContentRating;
+        private int timeShiftStatus;
+        private boolean overlayViewEnabled;
+
+        @Override
+        public void notifyChannelRetuned(Uri channelUri) {
+            this.channelUri = channelUri;
+        }
+
+        @Override
+        public void notifyTracksChanged(List<TvTrackInfo> tracks) {
+            this.tracks = tracks;
+        }
+
+        @Override
+        public void notifyTrackSelected(int type, String trackId) {
+            this.trackSelected = Pair.create(type, trackId);
+        }
+
+        @Override
+        public void notifyVideoAvailable() {
+            this.videoAvailable = true;
+        }
+
+        @Override
+        public void notifyVideoUnavailable(int reason) {
+            this.notifyVideoUnavailableReason = reason;
+        }
+
+        @Override
+        public void notifyContentAllowed() {
+            this.contentAllowed = true;
+        }
+
+        @Override
+        public void notifyContentBlocked(@NonNull TvContentRating rating) {
+            this.blockedContentRating = rating;
+        }
+
+        @Override
+        public void notifyTimeShiftStatusChanged(int status) {
+            this.timeShiftStatus = status;
+        }
+
+        @Override
+        public void setOverlayViewEnabled(boolean enabled) {
+            this.overlayViewEnabled = enabled;
+        }
+    }
+
+    private static final class TestSession extends TifSession {
+
+        private TestSession(TifSessionCallbacks callbacks) {
+            super(callbacks);
+        }
+
+        @Override
+        public boolean onSetSurface(@Nullable Surface surface) {
+            return false;
+        }
+
+        @Override
+        public void onSurfaceChanged(int format, int width, int height) {}
+
+        @Override
+        public void onSetStreamVolume(@FloatRange(from = 0.0, to = 1.0) float volume) {}
+
+        @Override
+        public boolean onTune(Uri channelUri) {
+            return false;
+        }
+
+        @Override
+        public void onSetCaptionEnabled(boolean enabled) {}
+
+        @Override
+        public void onUnblockContent(TvContentRating unblockedRating) {}
+
+        public void helpTestSetOverlayViewEnabled(boolean enabled) {
+            super.setOverlayViewEnabled(enabled);
+        }
+    }
+}
diff --git a/common/tests/robotests/src/com/android/tv/common/support/tis/WrappedSessionTest.java b/common/tests/robotests/src/com/android/tv/common/support/tis/WrappedSessionTest.java
new file mode 100644
index 0000000..6d09575
--- /dev/null
+++ b/common/tests/robotests/src/com/android/tv/common/support/tis/WrappedSessionTest.java
@@ -0,0 +1,188 @@
+package com.android.tv.common.support.tis;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.media.PlaybackParams;
+import android.media.tv.TvContentRating;
+import android.net.Uri;
+import android.os.Build;
+import android.view.View;
+import com.android.tv.common.support.tis.TifSession.TifSessionCallbacks;
+import com.android.tv.common.support.tis.TifSession.TifSessionFactory;
+import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link WrappedSession}. */
+@RunWith(GoogleRobolectricTestRunner.class)
+@Config(minSdk = Build.VERSION_CODES.M, maxSdk = Build.VERSION_CODES.P)
+public class WrappedSessionTest {
+
+    @Mock TifSession mockDelegate;
+    private TifSessionFactory sessionFactory =
+            new TifSessionFactory() {
+                @Override
+                public TifSession create(TifSessionCallbacks callbacks, String inputId) {
+                    return mockDelegate;
+                }
+            };
+    private WrappedSession wrappedSession;
+    private SimpleSessionManager sessionManager = new SimpleSessionManager(1);
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        wrappedSession =
+                new WrappedSession(
+                        RuntimeEnvironment.application,
+                        sessionManager,
+                        sessionFactory,
+                        "testInputId");
+    }
+
+    @Test
+    public void onRelease() {
+        sessionManager.addSession(wrappedSession);
+        assertThat(sessionManager.getSessionCount()).isEqualTo(1);
+        wrappedSession.onRelease();
+        assertThat(sessionManager.getSessionCount()).isEqualTo(0);
+        Mockito.verify(mockDelegate).onRelease();
+        Mockito.verifyNoMoreInteractions(mockDelegate);
+    }
+
+    @Test
+    public void onSetSurface() {
+        wrappedSession.onSetSurface(null);
+        Mockito.verify(mockDelegate).onSetSurface(null);
+        Mockito.verifyNoMoreInteractions(mockDelegate);
+    }
+
+    @Test
+    public void onSurfaceChanged() {
+        wrappedSession.onSurfaceChanged(1, 2, 3);
+        Mockito.verify(mockDelegate).onSurfaceChanged(1, 2, 3);
+        Mockito.verifyNoMoreInteractions(mockDelegate);
+    }
+
+    @Test
+    public void onSetStreamVolume() {
+        wrappedSession.onSetStreamVolume(.8f);
+        Mockito.verify(mockDelegate).onSetStreamVolume(.8f);
+        Mockito.verifyNoMoreInteractions(mockDelegate);
+    }
+
+    @Test
+    public void onTune() {
+        Uri uri = Uri.EMPTY;
+        wrappedSession.onTune(uri);
+        Mockito.verify(mockDelegate).onTune(uri);
+        Mockito.verifyNoMoreInteractions(mockDelegate);
+    }
+
+    @Test
+    public void onSetCaptionEnabled() {
+        wrappedSession.onSetCaptionEnabled(true);
+        Mockito.verify(mockDelegate).onSetCaptionEnabled(true);
+        Mockito.verifyNoMoreInteractions(mockDelegate);
+    }
+
+    @Test
+    @Config(minSdk = Build.VERSION_CODES.M)
+    public void onTimeShiftGetCurrentPosition() {
+        Mockito.when(mockDelegate.onTimeShiftGetCurrentPosition()).thenReturn(7L);
+        assertThat(wrappedSession.onTimeShiftGetCurrentPosition()).isEqualTo(7L);
+        Mockito.verify(mockDelegate).onTimeShiftGetCurrentPosition();
+        Mockito.verifyNoMoreInteractions(mockDelegate);
+    }
+
+    @Test
+    @Config(minSdk = Build.VERSION_CODES.M)
+    public void onTimeShiftGetStartPosition() {
+        Mockito.when(mockDelegate.onTimeShiftGetStartPosition()).thenReturn(8L);
+        assertThat(wrappedSession.onTimeShiftGetStartPosition()).isEqualTo(8L);
+        Mockito.verify(mockDelegate).onTimeShiftGetStartPosition();
+        Mockito.verifyNoMoreInteractions(mockDelegate);
+    }
+
+    @Test
+    @Config(minSdk = Build.VERSION_CODES.M)
+    public void onTimeShiftPause() {
+        wrappedSession.onTimeShiftPause();
+        Mockito.verify(mockDelegate).onTimeShiftPause();
+        Mockito.verifyNoMoreInteractions(mockDelegate);
+    }
+
+    @Test
+    @Config(minSdk = Build.VERSION_CODES.M)
+    public void onTimeShiftResume() {
+        wrappedSession.onTimeShiftResume();
+        Mockito.verify(mockDelegate).onTimeShiftResume();
+        Mockito.verifyNoMoreInteractions(mockDelegate);
+    }
+
+    @Test
+    @Config(minSdk = Build.VERSION_CODES.M)
+    public void onTimeShiftSeekTo() {
+        wrappedSession.onTimeShiftSeekTo(9L);
+        Mockito.verify(mockDelegate).onTimeShiftSeekTo(9L);
+        Mockito.verifyNoMoreInteractions(mockDelegate);
+    }
+
+    @Test
+    @Config(minSdk = Build.VERSION_CODES.M)
+    public void onTimeShiftSetPlaybackParams() {
+        PlaybackParams paras = new PlaybackParams();
+        wrappedSession.onTimeShiftSetPlaybackParams(paras);
+        Mockito.verify(mockDelegate).onTimeShiftSetPlaybackParams(paras);
+        Mockito.verifyNoMoreInteractions(mockDelegate);
+    }
+
+    @Test
+    @Config(minSdk = Build.VERSION_CODES.M)
+    public void onUnblockContent() {
+        TvContentRating rating =
+                TvContentRating.createRating(
+                        "domain", "rating_system", "rating", "subrating1", "subrating2");
+        wrappedSession.onUnblockContent(rating);
+        Mockito.verify(mockDelegate).onUnblockContent(rating);
+        Mockito.verifyNoMoreInteractions(mockDelegate);
+    }
+
+    @Test
+    public void onParentalControlsChanged() {
+        wrappedSession.onParentalControlsChanged();
+        Mockito.verify(mockDelegate).onParentalControlsChanged();
+        Mockito.verifyNoMoreInteractions(mockDelegate);
+    }
+
+    @Test
+    public void testSetOverlayViewEnabled() {
+        wrappedSession.setOverlayViewEnabled(true);
+        // Just verifying that the call completes.
+    }
+
+    @Test
+    public void testOnCreateOverlayView() {
+        View v = new View(RuntimeEnvironment.application);
+        Mockito.when(mockDelegate.onCreateOverlayView()).thenReturn(v);
+
+        View actualView = wrappedSession.onCreateOverlayView();
+
+        assertThat(actualView).isEqualTo(v);
+        Mockito.verify(mockDelegate).onCreateOverlayView();
+        Mockito.verifyNoMoreInteractions(mockDelegate);
+    }
+
+    @Test
+    public void testOnOverlayViewSizeChanged() {
+        wrappedSession.onOverlayViewSizeChanged(5 /* width */, 13 /* height */);
+        Mockito.verify(mockDelegate).onOverlayViewSizeChanged(5, 13);
+        Mockito.verifyNoMoreInteractions(mockDelegate);
+    }
+}
diff --git a/common/tests/robotests/src/com/android/tv/common/support/tvprovider/TvContractCompatXTest.java b/common/tests/robotests/src/com/android/tv/common/support/tvprovider/TvContractCompatXTest.java
new file mode 100644
index 0000000..db32be6
--- /dev/null
+++ b/common/tests/robotests/src/com/android/tv/common/support/tvprovider/TvContractCompatXTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.support.tvprovider;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.Uri;
+import com.android.tv.testing.constants.ConfigConstants;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** Test for {@link TvContractCompatX}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class TvContractCompatXTest {
+
+    @Test
+    public void buildChannelUri() {
+        assertThat(TvContractCompatX.buildChannelUri("com.example", "foo"))
+                .isEqualTo(
+                        Uri.parse(
+                                "content://android.media.tv/channel?"
+                                        + "package=com.example&internal_provider_id=foo"));
+    }
+
+    @Test
+    public void buildProgramsUriForChannel() {
+        assertThat(TvContractCompatX.buildProgramsUriForChannel("com.example", "foo"))
+                .isEqualTo(
+                        Uri.parse(
+                                "content://android.media.tv/program?"
+                                        + "package=com.example&internal_provider_id=foo"));
+    }
+
+    @Test
+    public void buildProgramsUriForChannel_period() {
+        assertThat(TvContractCompatX.buildProgramsUriForChannel("com.example", "foo", 1234L, 5467L))
+                .isEqualTo(
+                        Uri.parse(
+                                "content://android.media.tv/program?"
+                                        + "package=com.example&internal_provider_id=foo"
+                                        + "&start_time=1234&end_time=5467"));
+    }
+
+    @Test
+    public void buildProgramsUri_period() {
+        assertThat(TvContractCompatX.buildProgramsUri(1234L, 5467L))
+                .isEqualTo(
+                        Uri.parse(
+                                "content://android.media.tv/program?"
+                                        + "start_time=1234&end_time=5467"));
+    }
+}
diff --git a/common/tests/robotests/src/com/android/tv/common/util/CommonUtilsTest.java b/common/tests/robotests/src/com/android/tv/common/util/CommonUtilsTest.java
new file mode 100644
index 0000000..0b793db
--- /dev/null
+++ b/common/tests/robotests/src/com/android/tv/common/util/CommonUtilsTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.tv.testing.constants.ConfigConstants;
+import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner;
+import java.io.File;
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link CommonUtils}. */
+@RunWith(GoogleRobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class CommonUtilsTest {
+
+    @Test
+    public void deleteDirOrFile_file() throws IOException {
+        File file = new File(RuntimeEnvironment.application.getExternalFilesDir(null), "file");
+        assertThat(file.createNewFile()).isTrue();
+        assertThat(CommonUtils.deleteDirOrFile(file)).isTrue();
+        assertThat(file.exists()).isFalse();
+    }
+
+    @Test
+    public void deleteDirOrFile_Dir() throws IOException {
+        File dir = new File(RuntimeEnvironment.application.getExternalFilesDir(null), "dir");
+        assertThat(dir.mkdirs()).isTrue();
+        assertThat(new File(dir, "file").createNewFile()).isTrue();
+        assertThat(CommonUtils.deleteDirOrFile(dir)).isTrue();
+        assertThat(dir.exists()).isFalse();
+    }
+
+    @Test
+    public void deleteDirOrFile_unreadableDir() throws IOException {
+        File dir = new File(RuntimeEnvironment.application.getExternalFilesDir(null), "dir");
+        assertThat(dir.mkdirs()).isTrue();
+        assertThat(new File(dir, "file").createNewFile()).isTrue();
+        dir.setReadable(false);
+        // Since dir is unreadable dir.listFiles() returns null and file is not deleted thus
+        // dir is not actually deleted.
+        assertThat(CommonUtils.deleteDirOrFile(dir)).isFalse();
+        assertThat(dir.exists()).isTrue();
+    }
+
+    @Test
+    public void deleteDirOrFile_unreadableSubDir() throws IOException {
+        File dir = new File(RuntimeEnvironment.application.getExternalFilesDir(null), "dir");
+        File subDir = new File(dir, "sub");
+        assertThat(subDir.mkdirs()).isTrue();
+        File file = new File(subDir, "file");
+        assertThat(file.createNewFile()).isTrue();
+        subDir.setReadable(false);
+        // Since subDir is unreadable subDir.listFiles() returns null and file is not deleted thus
+        // dir is not actually deleted.
+        assertThat(CommonUtils.deleteDirOrFile(dir)).isFalse();
+        assertThat(dir.exists()).isTrue();
+    }
+}
diff --git a/common/tests/robotests/src/com/android/tv/common/util/ContentUriUtilsTest.java b/common/tests/robotests/src/com/android/tv/common/util/ContentUriUtilsTest.java
new file mode 100644
index 0000000..be705a9
--- /dev/null
+++ b/common/tests/robotests/src/com/android/tv/common/util/ContentUriUtilsTest.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.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.Uri;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link ContentUriUtils}. */
+@RunWith(GoogleRobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class ContentUriUtilsTest {
+
+    @Test
+    public void safeParseId_empty() {
+        assertThat(ContentUriUtils.safeParseId(Uri.EMPTY)).isEqualTo(-1);
+    }
+
+    @Test
+    public void safeParseId_bad() {
+        assertThat(ContentUriUtils.safeParseId(Uri.parse("foo/bad"))).isEqualTo(-1);
+    }
+
+    @Test
+    public void safeParseId_123() {
+        assertThat(ContentUriUtils.safeParseId(Uri.parse("foo/123"))).isEqualTo(123);
+    }
+}
diff --git a/gradle.properties b/gradle.properties
index 6208234..9d027fe 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -21,4 +21,5 @@
 
 org.gradle.daemon=true
 org.gradle.parallel=true
-org.gradle.configureondemand=true
\ No newline at end of file
+android.useAndroidX=true
+android.enableJetifier=true
\ No newline at end of file
diff --git a/jni/DvbManager.cpp b/jni/DvbManager.cpp
index f9dff59..3320959 100644
--- a/jni/DvbManager.cpp
+++ b/jni/DvbManager.cpp
@@ -42,7 +42,6 @@
           mDvrFd(-1),
           mPatFilterFd(-1),
           mDvbApiVersion(DVB_API_VERSION_UNDEFINED),
-          mDeliverySystemType(-1),
           mFeHasLock(false),
           mHasPendingTune(false) {
   jclass clazz = env->FindClass("com/android/tv/tuner/TunerHal");
@@ -50,6 +49,8 @@
       env->GetMethodID(clazz, "openDvbFrontEndFd", "()I");
   mOpenDvbDemuxMethodID = env->GetMethodID(clazz, "openDvbDemuxFd", "()I");
   mOpenDvbDvrMethodID = env->GetMethodID(clazz, "openDvbDvrFd", "()I");
+  memset(&mDeliverySystemTypes, DELIVERY_SYSTEM_UNDEFINED,
+      sizeof(mDeliverySystemTypes));
 }
 
 DvbManager::~DvbManager() {
@@ -115,6 +116,20 @@
 
 int DvbManager::tune(JNIEnv *env, jobject thiz,
         const int frequency, const char *modulationStr, int timeout_ms) {
+    return tuneInternal(env, thiz, DELIVERY_SYSTEM_UNDEFINED, frequency,
+               modulationStr, timeout_ms);
+}
+
+int DvbManager::tune(JNIEnv *env, jobject thiz,
+        const int deliverySystemType, const int frequency,
+        const char *modulationStr, int timeout_ms) {
+    return tuneInternal(env, thiz, deliverySystemType, frequency,
+               modulationStr, timeout_ms);
+}
+
+int DvbManager::tuneInternal(JNIEnv *env, jobject thiz,
+        const int deliverySystemType, const int frequency,
+        const char *modulationStr, int timeout_ms) {
     resetExceptFe();
 
     if (openDvbFe(env, thiz) != 0) {
@@ -146,10 +161,36 @@
         struct dtv_property deliverySystemProperty = {
             .cmd = DTV_DELIVERY_SYSTEM
         };
-        deliverySystemProperty.u.data = SYS_ATSC;
+        switch (deliverySystemType) {
+            case DELIVERY_SYSTEM_DVBT:
+                deliverySystemProperty.u.data = SYS_DVBT;
+                break;
+            case DELIVERY_SYSTEM_DVBT2:
+                deliverySystemProperty.u.data = SYS_DVBT2;
+                break;
+            case DELIVERY_SYSTEM_DVBS:
+                deliverySystemProperty.u.data = SYS_DVBS;
+                break;
+            case DELIVERY_SYSTEM_DVBS2:
+                deliverySystemProperty.u.data = SYS_DVBS2;
+                break;
+            case DELIVERY_SYSTEM_DVBC:
+                deliverySystemProperty.u.data = SYS_DVBC_ANNEX_A;
+                break;
+            case DELIVERY_SYSTEM_ATSC:
+            case DELIVERY_SYSTEM_UNDEFINED:
+                deliverySystemProperty.u.data = SYS_ATSC;
+                break;
+            default:
+                ALOGE("Unrecognized delivery system type");
+                return -1;
+        }
         struct dtv_property frequencyProperty = {
             .cmd = DTV_FREQUENCY
         };
+        struct dtv_property bandwidthProperty = {
+             .cmd = DTV_BANDWIDTH_HZ, .u.data = 8000000
+        };
         frequencyProperty.u.data = static_cast<__u32>(frequency);
         struct dtv_property modulationProperty = { .cmd = DTV_MODULATION };
         if (strncmp(modulationStr, "QAM", 3) == 0) {
@@ -163,10 +204,11 @@
         struct dtv_property tuneProperty = { .cmd = DTV_TUNE };
 
         struct dtv_property props[] = {
-                deliverySystemProperty, frequencyProperty, modulationProperty, tuneProperty
+                deliverySystemProperty, frequencyProperty, modulationProperty,
+                bandwidthProperty, tuneProperty
         };
         struct dtv_properties dtvProperty = {
-            .num = 4, .props = props
+            .num = sizeof(props)/sizeof(dtv_property), .props = props
         };
 
         if (mHasPendingTune) {
@@ -215,6 +257,9 @@
                     ALOGE("Unrecognized modulation mode : %s", modulationStr);
                     return -1;
                 }
+                feParams.u.ofdm.code_rate_HP = FEC_AUTO;
+                feParams.u.ofdm.code_rate_LP = FEC_AUTO;
+                feParams.u.ofdm.transmission_mode = TRANSMISSION_MODE_AUTO;
                 break;
             default:
                 ALOGE("Unsupported delivery system.");
@@ -462,22 +507,27 @@
 }
 
 int DvbManager::getDeliverySystemType(JNIEnv *env, jobject thiz) {
-    if (mDeliverySystemType != -1) {
-        return mDeliverySystemType;
+    getDeliverySystemTypes(env, thiz);
+    return mDeliverySystemTypes[0];
+}
+
+int* DvbManager::getDeliverySystemTypes(JNIEnv *env, jobject thiz) {
+    ALOGE("getDeliverySystemTypes");
+    if (mDeliverySystemTypes[0] != DELIVERY_SYSTEM_UNDEFINED) {
+        return mDeliverySystemTypes;
     }
     if (mFeFd == -1) {
         if ((mFeFd = openDvbFeFromSystemApi(env, thiz)) < 0) {
             ALOGD("Can't open FE file : %s", strerror(errno));
-            return DELIVERY_SYSTEM_UNDEFINED;
+            return mDeliverySystemTypes;
         }
     }
     struct dtv_property testProps[1] = {
-        { .cmd = DTV_DELIVERY_SYSTEM }
+        { .cmd = DTV_ENUM_DELSYS }
     };
     struct dtv_properties feProp = {
         .num = 1, .props = testProps
     };
-    mDeliverySystemType = DELIVERY_SYSTEM_UNDEFINED;
     if (ioctl(mFeFd, FE_GET_PROPERTY, &feProp) == -1) {
         mDvbApiVersion = DVB_API_VERSION3;
         if (openDvbFe(env, thiz) == 0) {
@@ -485,50 +535,52 @@
             if (ioctl(mFeFd, FE_GET_INFO, &info) == 0) {
                 switch (info.type) {
                     case FE_QPSK:
-                        mDeliverySystemType = DELIVERY_SYSTEM_DVBS;
+                        mDeliverySystemTypes[0] = DELIVERY_SYSTEM_DVBS;
                         break;
                     case FE_QAM:
-                        mDeliverySystemType = DELIVERY_SYSTEM_DVBC;
+                        mDeliverySystemTypes[0] = DELIVERY_SYSTEM_DVBC;
                         break;
                     case FE_OFDM:
-                        mDeliverySystemType = DELIVERY_SYSTEM_DVBT;
+                        mDeliverySystemTypes[0] = DELIVERY_SYSTEM_DVBT;
                         break;
                     case FE_ATSC:
-                        mDeliverySystemType = DELIVERY_SYSTEM_ATSC;
+                        mDeliverySystemTypes[0] = DELIVERY_SYSTEM_ATSC;
                         break;
                     default:
-                        mDeliverySystemType = DELIVERY_SYSTEM_UNDEFINED;
+                        mDeliverySystemTypes[0] = DELIVERY_SYSTEM_UNDEFINED;
                         break;
                 }
             }
         }
     } else {
         mDvbApiVersion = DVB_API_VERSION5;
-        switch (feProp.props[0].u.data) {
-            case SYS_DVBT:
-                mDeliverySystemType = DELIVERY_SYSTEM_DVBT;
-                break;
-            case SYS_DVBT2:
-                mDeliverySystemType = DELIVERY_SYSTEM_DVBT2;
-                break;
-            case SYS_DVBS:
-                mDeliverySystemType = DELIVERY_SYSTEM_DVBS;
-                break;
-            case SYS_DVBS2:
-                mDeliverySystemType = DELIVERY_SYSTEM_DVBS2;
-                break;
-            case SYS_DVBC_ANNEX_A:
-            case SYS_DVBC_ANNEX_B:
-            case SYS_DVBC_ANNEX_C:
-                mDeliverySystemType = DELIVERY_SYSTEM_DVBC;
-                break;
-            case SYS_ATSC:
-                mDeliverySystemType = DELIVERY_SYSTEM_ATSC;
-                break;
-            default:
-                mDeliverySystemType = DELIVERY_SYSTEM_UNDEFINED;
-                break;
+        for (unsigned int i = 0; i < feProp.props[0].u.buffer.len; i++) {
+            switch (feProp.props[0].u.buffer.data[i]) {
+                case SYS_DVBT:
+                    mDeliverySystemTypes[i] = DELIVERY_SYSTEM_DVBT;
+                    break;
+                case SYS_DVBT2:
+                    mDeliverySystemTypes[i] = DELIVERY_SYSTEM_DVBT2;
+                    break;
+                case SYS_DVBS:
+                    mDeliverySystemTypes[i] = DELIVERY_SYSTEM_DVBS;
+                    break;
+                case SYS_DVBS2:
+                    mDeliverySystemTypes[i] = DELIVERY_SYSTEM_DVBS2;
+                    break;
+                case SYS_DVBC_ANNEX_A:
+                case SYS_DVBC_ANNEX_B:
+                case SYS_DVBC_ANNEX_C:
+                    mDeliverySystemTypes[i] = DELIVERY_SYSTEM_DVBC;
+                    break;
+                case SYS_ATSC:
+                    mDeliverySystemTypes[i] = DELIVERY_SYSTEM_ATSC;
+                    break;
+                default:
+                    mDeliverySystemTypes[i] = DELIVERY_SYSTEM_UNDEFINED;
+                    break;
+            }
         }
     }
-    return mDeliverySystemType;
-}
\ No newline at end of file
+    return mDeliverySystemTypes;
+}
diff --git a/jni/DvbManager.h b/jni/DvbManager.h
index b01113e..aaa345e 100644
--- a/jni/DvbManager.h
+++ b/jni/DvbManager.h
@@ -63,7 +63,7 @@
     int mDvrFd;
     int mPatFilterFd;
     int mDvbApiVersion;
-    int mDeliverySystemType;
+    int mDeliverySystemTypes[8];
     bool mFeHasLock;
     // Flag for pending tune request. Used for canceling the current tune operation.
     bool volatile mHasPendingTune;
@@ -78,6 +78,9 @@
     ~DvbManager();
     int tune(JNIEnv *env, jobject thiz,
             const int frequency, const char *modulationStr, int timeout_ms);
+    int tune(JNIEnv *env, jobject thiz,
+            const int deliverySystemType, const int frequency,
+            const char *modulationStr, int timeout_ms);
     int stopTune();
     int readTsStream(JNIEnv *env, jobject thiz,
             uint8_t *tsBuffer, int tsBufferSize, int timeout_ms);
@@ -85,9 +88,13 @@
     void closeAllDvbPidFilter();
     void setHasPendingTune(bool hasPendingTune);
     int getDeliverySystemType(JNIEnv *env, jobject thiz);
+    int *getDeliverySystemTypes(JNIEnv *env, jobject thiz);
     int getSignalStrength();
 
 private:
+    int tuneInternal(JNIEnv *env, jobject thiz,
+            const int deliverySystemType, const int frequency,
+            const char *modulationStr, int timeout_ms);
     int openDvbFe(JNIEnv *env, jobject thiz);
     int openDvbDvr(JNIEnv *env, jobject thiz);
     void closePatFilter();
diff --git a/jni/tunertvinput_jni.cpp b/jni/tunertvinput_jni.cpp
index 030f961..579b92e 100644
--- a/jni/tunertvinput_jni.cpp
+++ b/jni/tunertvinput_jni.cpp
@@ -49,11 +49,12 @@
 /*
  * Class:     com_android_tv_tuner_TunerHal
  * Method:    nativeTune
- * Signature: (JILjava/lang/String;)Z
+ * Signature: (JILjava/lang/String;I)Z
  */
-JNIEXPORT jboolean JNICALL Java_com_android_tv_tuner_TunerHal_nativeTune(
-    JNIEnv *env, jobject thiz, jlong deviceId, jint frequency,
-    jstring modulation, jint timeout_ms) {
+JNIEXPORT jboolean JNICALL
+    Java_com_android_tv_tuner_TunerHal_nativeTune__JILjava_lang_String_2I(
+        JNIEnv *env, jobject thiz, jlong deviceId, jint frequency,
+        jstring modulation, jint timeout_ms) {
   std::map<jlong, DvbManager *>::iterator it = sDvbManagers.find(deviceId);
   DvbManager *dvbManager;
   if (it == sDvbManagers.end()) {
@@ -69,6 +70,29 @@
 
 /*
  * Class:     com_android_tv_tuner_TunerHal
+ * Method:    nativeTune
+ * Signature: (JIILjava/lang/String;I)Z
+ */
+JNIEXPORT jboolean JNICALL
+    Java_com_android_tv_tuner_TunerHal_nativeTune__JIILjava_lang_String_2I(
+        JNIEnv *env, jobject thiz, jlong deviceId, jint deliverySystemType,
+        jint frequency, jstring modulation, jint timeout_ms) {
+    std::map<jlong, DvbManager *>::iterator it = sDvbManagers.find(deviceId);
+    DvbManager *dvbManager;
+    if (it == sDvbManagers.end()) {
+        dvbManager = new DvbManager(env, thiz);
+        sDvbManagers.insert(
+            std::pair<jlong, DvbManager *>(deviceId, dvbManager));
+    } else {
+        dvbManager = it->second;
+    }
+    int res = dvbManager->tune(env, thiz, deliverySystemType, frequency,
+                env->GetStringUTFChars(modulation, 0), timeout_ms);
+    return (res == 0);
+}
+
+/*
+ * Class:     com_android_tv_tuner_TunerHal
  * Method:    nativeCloseAllPidFilters
  * Signature: (J)V
  */
@@ -190,4 +214,32 @@
     sDvbManagers.insert(std::pair<jlong, DvbManager *>(deviceId, dvbManager));
     return dvbManager->getDeliverySystemType(env, thiz);
   }
-}
\ No newline at end of file
+}
+
+/*
+ * Class:     com_android_tv_tuner_TunerHal
+ * Method:    nativeGetDeliverySystemTypes
+ * Signature: (J)I
+ */
+JNIEXPORT jintArray JNICALL
+Java_com_android_tv_tuner_TunerHal_nativeGetDeliverySystemTypes(JNIEnv *env,
+                                                               jobject thiz,
+                                                               jlong deviceId) {
+    jintArray deliverySystemTypes = env->NewIntArray(8);
+    if (deliverySystemTypes == NULL) {
+        ALOGE("Out of memory!");
+        return NULL;
+    }
+    std::map<jlong, DvbManager *>::iterator it = sDvbManagers.find(deviceId);
+    if (it != sDvbManagers.end()) {
+        env->SetIntArrayRegion(deliverySystemTypes, 0, 8,
+        it->second->getDeliverySystemTypes(env, thiz));
+    } else {
+        DvbManager *dvbManager = new DvbManager(env, thiz);
+        sDvbManagers.insert(
+            std::pair<jlong, DvbManager *>(deviceId, dvbManager));
+        env->SetIntArrayRegion(deliverySystemTypes, 0, 8,
+        dvbManager->getDeliverySystemTypes(env, thiz));
+    }
+    return deliverySystemTypes;
+}
diff --git a/jni/tunertvinput_jni.h b/jni/tunertvinput_jni.h
index 36e631f..02c242b 100755
--- a/jni/tunertvinput_jni.h
+++ b/jni/tunertvinput_jni.h
@@ -66,8 +66,18 @@
  * 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__JILjava_lang_String_2I
+        (JNIEnv *, jobject, jlong, jint, jstring, jint);
+
+/*
+ * Class:     com_android_tv_tuner_TunerHal
+ * Method:    nativeTune
+ * Signature: Signature: (JIILjava/lang/String;I)Z
+ */
+JNIEXPORT jboolean JNICALL
+    Java_com_android_tv_tuner_TunerHal_nativeTune__JIILjava_lang_String_2I
+        (JNIEnv *, jobject, jlong, jint, jint, jstring, jint);
 
 /*
  * Class:     com_android_tv_tuner_TunerHal
@@ -106,6 +116,15 @@
 
 /*
  * Class:     com_android_tv_tuner_TunerHal
+ * Method:    nativeGetDeliverySystemTypes
+ * Signature: (J)I
+ */
+JNIEXPORT jintArray JNICALL
+Java_com_android_tv_tuner_TunerHal_nativeGetDeliverySystemTypes
+  (JNIEnv *, jobject, jlong);
+
+/*
+ * Class:     com_android_tv_tuner_TunerHal
  * Method:    nativeGetSignalStrength
  * Signature: (J)I
  */
diff --git a/libs/Android.bp b/libs/Android.bp
index fea9487..6e6c0f4 100644
--- a/libs/Android.bp
+++ b/libs/Android.bp
@@ -13,8 +13,15 @@
 // limitations under the License.
 
 java_import {
+    name: "tv-auto-common-jar",
+    jars: ["m2/auto-common-0.10.jar"],
+    host_supported: true,
+    sdk_version: "current",
+}
+
+java_import {
     name: "tv-auto-factory-jar",
-    jars: ["auto-factory-1.0-beta2.jar"],
+    jars: ["m2/auto-factory-1.0-beta6.jar"],
     host_supported: true,
     sdk_version: "current",
 }
@@ -22,20 +29,22 @@
 java_plugin {
     name: "tv-auto-factory",
     static_libs: [
-	"jsr330",
+        "jsr330",
+        "tv-auto-common-jar",
         "tv-auto-factory-jar",
+        "tv-auto-value-jar",
+        "tv-google-java-format-jar",
         "tv-guava-jre-jar",
-	"tv-javawriter-jar",
-	"tv-javax-annotations-jar",
+        "tv-javapoet-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"],
+    jars: ["m2/auto-value-1.5.3.jar"],
     host_supported: true,
     sdk_version: "current",
 }
@@ -51,26 +60,33 @@
 
 java_import {
     name: "tv-error-prone-annotations-jar",
-    jars: ["error_prone_annotations-2.3.1.jar"],
+    jars: ["m2/error_prone_annotations-2.3.2.jar"],
     sdk_version: "current",
 }
 
 java_import {
-    name: "tv-guava-jre-jar",
-    jars: ["guava-23.3-jre.jar"],
+    name: "tv-google-java-format-jar",
+    jars: ["google-java-format-1.7-all-deps.jar"],
     host_supported: true,
     sdk_version: "current",
 }
 
 java_import {
     name: "tv-guava-android-jar",
-    jars: ["guava-23.6-android.jar"],
+    jars: ["m2/guava-28.0-android.jar"],
     sdk_version: "current",
 }
 
-java_import_host{
-    name: "tv-javawriter-jar",
-    jars: ["javawriter-2.5.1.jar"],
+java_import {
+    name: "tv-guava-jre-jar",
+    jars: ["m2/guava-28.0-jre.jar"],
+    host_supported: true,
+    sdk_version: "current",
+}
+
+java_import_host {
+    name: "tv-javapoet-jar",
+    jars: ["m2/javapoet-1.11.1.jar"],
 }
 
 java_import {
@@ -80,7 +96,6 @@
     sdk_version: "current",
 }
 
-
 android_library_import {
     name: "tv-lib-exoplayer",
     aars: ["exoplayer-r1.5.16.aar"],
@@ -89,31 +104,22 @@
 
 android_library_import {
     name: "tv-lib-exoplayer-v2-core",
-    aars: ["exoplayer-core-2.9.0.aar"],
+    aars: ["exoplayer-core-2.10.1.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",
+        "m2/dagger-compiler-2.23.jar",
+        "m2/dagger-producers-2.23.jar",
+        "m2/dagger-spi-2.23.jar",
     ],
 }
 
 java_import {
     name: "tv-lib-dagger",
-    jars: ["dagger-2.15.jar"],
+    jars: ["m2/dagger-2.23.jar"],
     host_supported: true,
     sdk_version: "current",
 }
@@ -122,26 +128,31 @@
     name: "tv-lib-dagger-compiler",
     static_libs: [
         "tv-lib-dagger-compiler-import",
-        "tv-lib-dagger-compiler-deps",
+        "tv-guava-jre-jar",
+        "tv-javapoet-jar",
+        "tv-google-java-format-jar",
         "jsr330",
         "tv-lib-dagger",
     ],
     processor_class: "dagger.internal.codegen.ComponentProcessor",
     generates_api: true,
+      // shade guava to avoid conflicts with guava embedded in Error Prone.
+        jarjar_rules: "m2/dagger-jarjar-rules.txt",
 }
 
 android_library_import {
     name: "tv-lib-dagger-android",
-    aars: ["dagger-android-2.15.aar"],
+    aars: ["m2/dagger-android-2.23.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",
+        "m2/dagger-android-jarimpl-2.23.jar",
+        "m2/dagger-android-processor-2.23.jar",
+        "m2/dagger-spi-2.23.jar",
+        "m2/protobuf-java-3.7.0.jar",
     ],
 }
 
@@ -149,17 +160,21 @@
     name: "tv-lib-dagger-android-processor",
     static_libs: [
         "tv-lib-dagger-android-processor-import",
-        "tv-lib-dagger-compiler-deps",
+        "tv-guava-jre-jar",
+        "tv-javapoet-jar",
+        "tv-google-java-format-jar",
         "jsr330",
         "tv-lib-dagger",
     ],
     processor_class: "dagger.android.processor.AndroidProcessor",
     generates_api: true,
+          // shade guava to avoid conflicts with guava embedded in Error Prone.
+            jarjar_rules: "m2/dagger-jarjar-rules.txt",
 }
 
 java_import {
     name: "tv-lib-truth",
-    jars: ["truth-0.36.jar"],
+    jars: ["truth-0.45.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
deleted file mode 100644
index ceaddac..0000000
--- a/libs/auto-factory-1.0-beta2.jar
+++ /dev/null
Binary files differ
diff --git a/libs/auto-value-1.5.2.jar b/libs/auto-value-1.5.2.jar
deleted file mode 100644
index 8ac0567..0000000
--- a/libs/auto-value-1.5.2.jar
+++ /dev/null
Binary files differ
diff --git a/libs/dagger-2.15.jar b/libs/dagger-2.15.jar
deleted file mode 100644
index 6d76688..0000000
--- a/libs/dagger-2.15.jar
+++ /dev/null
Binary files differ
diff --git a/libs/dagger-android-2.15.aar b/libs/dagger-android-2.15.aar
deleted file mode 100644
index 430294a..0000000
--- a/libs/dagger-android-2.15.aar
+++ /dev/null
Binary files differ
diff --git a/libs/dagger-android-jarimpl-2.15.jar b/libs/dagger-android-jarimpl-2.15.jar
deleted file mode 100644
index 7f7cd45..0000000
--- a/libs/dagger-android-jarimpl-2.15.jar
+++ /dev/null
Binary files differ
diff --git a/libs/dagger-android-processor-2.15.jar b/libs/dagger-android-processor-2.15.jar
deleted file mode 100644
index 3c7ac05..0000000
--- a/libs/dagger-android-processor-2.15.jar
+++ /dev/null
Binary files differ
diff --git a/libs/dagger-android-support-2.15.aar b/libs/dagger-android-support-2.15.aar
deleted file mode 100644
index 89a71a9..0000000
--- a/libs/dagger-android-support-2.15.aar
+++ /dev/null
Binary files differ
diff --git a/libs/dagger-android-support-jarimpl-2.15.jar b/libs/dagger-android-support-jarimpl-2.15.jar
deleted file mode 100644
index d0ea01a..0000000
--- a/libs/dagger-android-support-jarimpl-2.15.jar
+++ /dev/null
Binary files differ
diff --git a/libs/dagger-compiler-2.15.jar b/libs/dagger-compiler-2.15.jar
deleted file mode 100644
index e73110f..0000000
--- a/libs/dagger-compiler-2.15.jar
+++ /dev/null
Binary files differ
diff --git a/libs/dagger-producers-2.15.jar b/libs/dagger-producers-2.15.jar
deleted file mode 100644
index f1dbb07..0000000
--- a/libs/dagger-producers-2.15.jar
+++ /dev/null
Binary files differ
diff --git a/libs/dagger-spi-2.15.jar b/libs/dagger-spi-2.15.jar
deleted file mode 100644
index 6e3156a..0000000
--- a/libs/dagger-spi-2.15.jar
+++ /dev/null
Binary files differ
diff --git a/libs/error_prone_annotations-2.3.1.jar b/libs/error_prone_annotations-2.3.1.jar
deleted file mode 100644
index 8a0efa3..0000000
--- a/libs/error_prone_annotations-2.3.1.jar
+++ /dev/null
Binary files differ
diff --git a/libs/exoplayer-core-2.10.1.aar b/libs/exoplayer-core-2.10.1.aar
new file mode 100644
index 0000000..2342fed
--- /dev/null
+++ b/libs/exoplayer-core-2.10.1.aar
Binary files differ
diff --git a/libs/exoplayer-core-2.9.0.aar b/libs/exoplayer-core-2.9.0.aar
deleted file mode 100644
index 64c4f37..0000000
--- a/libs/exoplayer-core-2.9.0.aar
+++ /dev/null
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
deleted file mode 100644
index b10bfbd..0000000
--- a/libs/google-java-format-1.4-all-deps.jar
+++ /dev/null
Binary files differ
diff --git a/libs/google-java-format-1.7-all-deps.jar b/libs/google-java-format-1.7-all-deps.jar
new file mode 100644
index 0000000..e2d40de
--- /dev/null
+++ b/libs/google-java-format-1.7-all-deps.jar
Binary files differ
diff --git a/libs/guava-23.3-jre.jar b/libs/guava-23.3-jre.jar
deleted file mode 100644
index b13e275..0000000
--- a/libs/guava-23.3-jre.jar
+++ /dev/null
Binary files differ
diff --git a/libs/guava-23.5-jre.jar b/libs/guava-23.5-jre.jar
deleted file mode 100644
index 7e5f13a..0000000
--- a/libs/guava-23.5-jre.jar
+++ /dev/null
Binary files differ
diff --git a/libs/guava-23.6-android.jar b/libs/guava-23.6-android.jar
deleted file mode 100644
index 01180d2..0000000
--- a/libs/guava-23.6-android.jar
+++ /dev/null
Binary files differ
diff --git a/libs/javapoet-1.8.0.jar b/libs/javapoet-1.8.0.jar
deleted file mode 100644
index 6758b6d..0000000
--- a/libs/javapoet-1.8.0.jar
+++ /dev/null
Binary files differ
diff --git a/libs/javawriter-2.5.1.jar b/libs/javawriter-2.5.1.jar
deleted file mode 100644
index 4ec579e..0000000
--- a/libs/javawriter-2.5.1.jar
+++ /dev/null
Binary files differ
diff --git a/libs/m2/animal-sniffer-annotations-1.17.jar b/libs/m2/animal-sniffer-annotations-1.17.jar
new file mode 100644
index 0000000..6ec7a60
--- /dev/null
+++ b/libs/m2/animal-sniffer-annotations-1.17.jar
Binary files differ
diff --git a/libs/m2/auto-common-0.10.jar b/libs/m2/auto-common-0.10.jar
new file mode 100644
index 0000000..8cbfa72
--- /dev/null
+++ b/libs/m2/auto-common-0.10.jar
Binary files differ
diff --git a/libs/m2/auto-factory-1.0-beta6.jar b/libs/m2/auto-factory-1.0-beta6.jar
new file mode 100644
index 0000000..e47130f
--- /dev/null
+++ b/libs/m2/auto-factory-1.0-beta6.jar
Binary files differ
diff --git a/libs/m2/auto-value-1.5.3.jar b/libs/m2/auto-value-1.5.3.jar
new file mode 100644
index 0000000..99eeb0b
--- /dev/null
+++ b/libs/m2/auto-value-1.5.3.jar
Binary files differ
diff --git a/libs/m2/checker-qual-2.8.1.jar b/libs/m2/checker-qual-2.8.1.jar
new file mode 100644
index 0000000..09269be
--- /dev/null
+++ b/libs/m2/checker-qual-2.8.1.jar
Binary files differ
diff --git a/libs/m2/dagger-2.23.jar b/libs/m2/dagger-2.23.jar
new file mode 100644
index 0000000..544ee3d
--- /dev/null
+++ b/libs/m2/dagger-2.23.jar
Binary files differ
diff --git a/libs/m2/dagger-android-2.23.aar b/libs/m2/dagger-android-2.23.aar
new file mode 100644
index 0000000..9578dcd
--- /dev/null
+++ b/libs/m2/dagger-android-2.23.aar
Binary files differ
diff --git a/libs/m2/dagger-android-jarimpl-2.23.jar b/libs/m2/dagger-android-jarimpl-2.23.jar
new file mode 100644
index 0000000..94a2bbe
--- /dev/null
+++ b/libs/m2/dagger-android-jarimpl-2.23.jar
Binary files differ
diff --git a/libs/m2/dagger-android-processor-2.23.jar b/libs/m2/dagger-android-processor-2.23.jar
new file mode 100644
index 0000000..500149c
--- /dev/null
+++ b/libs/m2/dagger-android-processor-2.23.jar
Binary files differ
diff --git a/libs/m2/dagger-compiler-2.23.jar b/libs/m2/dagger-compiler-2.23.jar
new file mode 100644
index 0000000..b7cb162
--- /dev/null
+++ b/libs/m2/dagger-compiler-2.23.jar
Binary files differ
diff --git a/libs/m2/dagger-jarjar-rules.txt b/libs/m2/dagger-jarjar-rules.txt
new file mode 100644
index 0000000..618c243
--- /dev/null
+++ b/libs/m2/dagger-jarjar-rules.txt
@@ -0,0 +1,4 @@
+# shade guava to avoid conflicts with guava embedded in Error Prone.
+rule com.google.common.** com.google.dagger.common.@1
+rule com.google.auto.** com.google.dagger.auto.@1
+
diff --git a/libs/m2/dagger-producers-2.23.jar b/libs/m2/dagger-producers-2.23.jar
new file mode 100644
index 0000000..cb1cef9
--- /dev/null
+++ b/libs/m2/dagger-producers-2.23.jar
Binary files differ
diff --git a/libs/m2/dagger-spi-2.23.jar b/libs/m2/dagger-spi-2.23.jar
new file mode 100644
index 0000000..6af1494
--- /dev/null
+++ b/libs/m2/dagger-spi-2.23.jar
Binary files differ
diff --git a/libs/m2/error_prone_annotations-2.3.2.jar b/libs/m2/error_prone_annotations-2.3.2.jar
new file mode 100644
index 0000000..bc2584d
--- /dev/null
+++ b/libs/m2/error_prone_annotations-2.3.2.jar
Binary files differ
diff --git a/libs/m2/failureaccess-1.0.1.jar b/libs/m2/failureaccess-1.0.1.jar
new file mode 100644
index 0000000..9b56dc7
--- /dev/null
+++ b/libs/m2/failureaccess-1.0.1.jar
Binary files differ
diff --git a/libs/m2/guava-28.0-android.jar b/libs/m2/guava-28.0-android.jar
new file mode 100644
index 0000000..516fc5f
--- /dev/null
+++ b/libs/m2/guava-28.0-android.jar
Binary files differ
diff --git a/libs/m2/guava-28.0-jre.jar b/libs/m2/guava-28.0-jre.jar
new file mode 100644
index 0000000..f254aae
--- /dev/null
+++ b/libs/m2/guava-28.0-jre.jar
Binary files differ
diff --git a/libs/m2/j2objc-annotations-1.3.jar b/libs/m2/j2objc-annotations-1.3.jar
new file mode 100644
index 0000000..a429c72
--- /dev/null
+++ b/libs/m2/j2objc-annotations-1.3.jar
Binary files differ
diff --git a/libs/m2/javac-shaded-9-dev-r4023-3.jar b/libs/m2/javac-shaded-9-dev-r4023-3.jar
new file mode 100644
index 0000000..d7b3fd8
--- /dev/null
+++ b/libs/m2/javac-shaded-9-dev-r4023-3.jar
Binary files differ
diff --git a/libs/m2/javapoet-1.11.1.jar b/libs/m2/javapoet-1.11.1.jar
new file mode 100644
index 0000000..27a18e8
--- /dev/null
+++ b/libs/m2/javapoet-1.11.1.jar
Binary files differ
diff --git a/libs/m2/jsr305-3.0.2.jar b/libs/m2/jsr305-3.0.2.jar
new file mode 100644
index 0000000..59222d9
--- /dev/null
+++ b/libs/m2/jsr305-3.0.2.jar
Binary files differ
diff --git a/libs/m2/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar b/libs/m2/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar
new file mode 100644
index 0000000..45832c0
--- /dev/null
+++ b/libs/m2/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar
Binary files differ
diff --git a/libs/m2/pom-jre.xml b/libs/m2/pom-jre.xml
new file mode 100644
index 0000000..2d834c3
--- /dev/null
+++ b/libs/m2/pom-jre.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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
+  -->
+
+<!-- JRE version of libs, in particular guava -->
+<project>
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>com.android.tv</groupId>
+    <artifactId>jre-libs</artifactId>
+    <version>1</version>
+
+    <repositories>
+        <repository>
+            <id>google</id>
+            <name>Google Maven Repository</name>
+            <url>https://maven.google.com</url>
+            <layout>default</layout>
+            <snapshots>
+                <enabled>false</enabled>
+            </snapshots>
+        </repository>
+    </repositories>
+
+    <dependencies>
+
+
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>28.0-jre</version>
+        </dependency>
+
+    </dependencies>
+</project>
\ No newline at end of file
diff --git a/libs/m2/pom.xml b/libs/m2/pom.xml
new file mode 100644
index 0000000..5a232d5
--- /dev/null
+++ b/libs/m2/pom.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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
+  -->
+<project>
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>com.android.tv</groupId>
+    <artifactId>libs</artifactId>
+    <version>1</version>
+
+    <repositories>
+        <repository>
+            <id>google</id>
+            <name>Google Maven Repository</name>
+            <url>https://maven.google.com</url>
+            <layout>default</layout>
+            <snapshots>
+                <enabled>false</enabled>
+            </snapshots>
+        </repository>
+    </repositories>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.google.auto</groupId>
+            <artifactId>auto-common</artifactId>
+            <version>0.10</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.auto.factory</groupId>
+            <artifactId>auto-factory</artifactId>
+            <version>1.0-beta6</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.dagger</groupId>
+            <artifactId>dagger</artifactId>
+            <version>2.23</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.dagger</groupId>
+            <artifactId>dagger-android</artifactId>
+            <type>aar</type>
+            <version>2.23</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.dagger</groupId>
+            <artifactId>dagger-android-jarimpl</artifactId>
+            <version>2.23</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.dagger</groupId>
+            <artifactId>dagger-android-processor</artifactId>
+            <version>2.23</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.dagger</groupId>
+            <artifactId>dagger-compiler</artifactId>
+            <version>2.23</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.dagger</groupId>
+            <artifactId>dagger-producers</artifactId>
+            <version>2.23</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.dagger</groupId>
+            <artifactId>dagger-spi</artifactId>
+            <version>2.23</version>
+        </dependency>
+
+
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>28.0-android</version>
+        </dependency>
+
+        <!-- The host version of guava is listed in pom-jre.xml -->
+
+        <dependency>
+            <groupId>com.squareup</groupId>
+            <artifactId>javapoet</artifactId>
+            <version>1.11.1</version>
+        </dependency>
+    </dependencies>
+</project>
\ No newline at end of file
diff --git a/libs/m2/protobuf-java-3.7.0.jar b/libs/m2/protobuf-java-3.7.0.jar
new file mode 100644
index 0000000..eebaefe
--- /dev/null
+++ b/libs/m2/protobuf-java-3.7.0.jar
Binary files differ
diff --git a/libs/m2/update.sh b/libs/m2/update.sh
new file mode 100755
index 0000000..ee455c7
--- /dev/null
+++ b/libs/m2/update.sh
@@ -0,0 +1,37 @@
+#!/bin/bash
+#
+# 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.
+
+
+git rm *.jar *.aar
+
+EXCLUDES=google-java-format,javax.inject,support-annotations,jsr250-api,checker-compat-qual
+
+mvn \
+  -DoutputDirectory=${pwd}  \
+  dependency:copy-dependencies \
+  -DincludeScope=runtime \
+  -DexcludeArtifactIds=${EXCLUDES}
+
+mvn \
+  -DoutputDirectory=${pwd}  \
+  -f pom-jre.xml \
+  dependency:copy-dependencies \
+  -DincludeScope=runtime \
+  -DexcludeArtifactIds=${EXCLUDES}
+
+git add *.jar *.aar
+
+
diff --git a/libs/truth-0.36.jar b/libs/truth-0.36.jar
deleted file mode 100644
index 8174e4a..0000000
--- a/libs/truth-0.36.jar
+++ /dev/null
Binary files differ
diff --git a/libs/truth-0.45.jar b/libs/truth-0.45.jar
new file mode 100644
index 0000000..76e4da8
--- /dev/null
+++ b/libs/truth-0.45.jar
Binary files differ
diff --git a/partner_support/AndroidManifest.xml b/partner_support/AndroidManifest.xml
index 45a693f..8f80708 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="27" android:minSdkVersion="23"/>
+  <uses-sdk android:targetSdkVersion="28" android:minSdkVersion="23"/>
   <application />
 </manifest>
diff --git a/partner_support/g3doc/CloudEpgForPartners.md b/partner_support/g3doc/CloudEpgForPartners.md
deleted file mode 100644
index bec6b50..0000000
--- a/partner_support/g3doc/CloudEpgForPartners.md
+++ /dev/null
@@ -1,112 +0,0 @@
-# 3rd party instructions for using Cloud EPG feature of Live Channels
-
-Partners can ask Live Channels to retrieve EPG data for their TV Input Service
-using live channels
-
-## Prerequisites
-
-*   Updated agreement with Google
-*   Oreo or patched Nougat
-
-## Nougat
-
-To use cloud epg with Nougat you will need the following changes.
-
-### Patch TVProvider
-
-To run in Nougat you must cherry pick [change
-455319](https://android-review.googlesource.com/c/platform/packages/providers/TvProvider/+/455319)
-to TV Provider.
-
-### Customisation
-
-Indicate TvProvider is patched by including the following in their TV
-customization resource
-
-```
-<bool name="tvprovider_allows_system_inserts_to_program_table">true</bool>
-```
-
-See https://source.android.com/devices/tv/customize-tv-app
-
-## **Input Setup**
-
-During the input setup activity, the TIS will query the content provider for
-lineups in a given postal code. The TIS then inserts a row to the inputs table
-with input_id and lineup_id
-
-On completion of the activity the TIS sets the extra data in the result to
-
-*   `com.android.tv.extra.USE_CLOUD_EPG = true`
-*   `TvInputInfo.EXTRA_INPUT_ID` with their input_id
-
-This is used to tell Live Channels to immediately start the EPG fetch for that
-input.
-
-### Sample Input Setup code.
-
-A complete sample is at
-../third_party/samples/src/com/example/partnersupportsampletvinput
-
-#### query lineup
-
-```java
- private AsyncTask<Void, Void, List<Lineup>> createFetchLineupsTask() {
-        return new AsyncTask<Void, Void, List<Lineup>>() {
-            @Override
-            protected List<Lineup> doInBackground(Void... params) {
-                ContentResolver cr = getActivity().getContentResolver();
-
-                List<Lineup> results = new ArrayList<>();
-                Cursor cursor =
-                        cr.query(
-                                Uri.parse(
-                                        "content://com.android.tv.data.epg/lineups/postal_code/"
-                                                + ZIP),
-                                null,
-                                null,
-                                null,
-                                null);
-
-                while (cursor.moveToNext()) {
-                    String id = cursor.getString(0);
-                    String name = cursor.getString(1);
-                    String channels = cursor.getString(2);
-                    results.add(new Lineup(id, name, channels));
-                }
-
-                return results;
-            }
-
-            @Override
-            protected void onPostExecute(List<Lineup> lineups) {
-                showLineups(lineups);
-            }
-        };
-    }
-```
-
-#### Insert cloud_epg_input
-
-```java
-ContentValues values = new ContentValues();
-values.put(EpgContract.EpgInputs.COLUMN_INPUT_ID, SampleTvInputService.INPUT_ID);
-values.put(EpgContract.EpgInputs.COLUMN_LINEUP_ID, lineup.getId());
-ContentResolver contentResolver = getActivity().getContentResolver();
-EpgInput epgInput = EpgInputs.queryEpgInput(contentResolver, SampleTvInputService.INPUT_ID);
-if (epgInput == null) {
-    contentResolver.insert(EpgContract.EpgInputs.CONTENT_URI, values);
-} else {
-    values.put(EpgContract.EpgInputs.COLUMN_ID, epgInput.getId());
-    EpgInputs.update(contentResolver, EpgInput.createEpgChannel(values));
-}
-```
-
-#### Return use_cloud_epg
-
-```java
-Intent data = new Intent();
-data.putExtra(TvInputInfo.EXTRA_INPUT_ID, inputId);
-data.putExtra(com.android.tv.extra.USE_CLOUD_EPG, true);
-setResult(Activity.RESULT_OK, data);
-```
diff --git a/partner_support/g3doc/SeriesIdColumnForPartners.md b/partner_support/g3doc/SeriesIdColumnForPartners.md
deleted file mode 100644
index cd44db0..0000000
--- a/partner_support/g3doc/SeriesIdColumnForPartners.md
+++ /dev/null
@@ -1,30 +0,0 @@
-# 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
deleted file mode 100644
index 0ba7cff..0000000
--- a/partner_support/g3doc/TurnOffEmbeddedTuner.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# 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 804691a..5e4c2c7 100644
--- a/partner_support/sample_customization/AndroidManifest.xml
+++ b/partner_support/sample_customization/AndroidManifest.xml
@@ -24,7 +24,7 @@
     <uses-feature android:name="android.software.leanback" android:required="true" />
     <uses-feature android:name="android.hardware.touchscreen" android:required="false" />
 
-    <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
+    <uses-sdk android:targetSdkVersion="28" android:minSdkVersion="23"/>
 
     <application android:label="Partner Customization"
             android:theme="@android:style/Theme.Holo.Light.NoActionBar"
diff --git a/partner_support/samples/Android.bp b/partner_support/samples/Android.bp
new file mode 100644
index 0000000..9c1d2db
--- /dev/null
+++ b/partner_support/samples/Android.bp
@@ -0,0 +1,25 @@
+android_app {
+    name: "PartnerSupportSampleTvInput",
+
+    // Include all java files.
+    srcs: ["src/**/*.java"],
+
+    static_libs: [
+        "androidx.leanback_leanback",
+        "androidx.tvprovider_tvprovider",
+        "live-channels-partner-support",
+    ],
+
+    optimize: {
+        enabled: false,
+    },
+    // Overlay view related functionality requires system APIs.
+    sdk_version: "system_current",
+    min_sdk_version: "23", // M
+
+    // Required for com.android.tv.permission.RECEIVE_INPUT_EVENT
+    privileged: true,
+
+    resource_dirs: ["res"],
+
+}
diff --git a/partner_support/samples/Android.mk b/partner_support/samples/Android.mk
deleted file mode 100644
index 2e771a5..0000000
--- a/partner_support/samples/Android.mk
+++ /dev/null
@@ -1,33 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-# Include all java files.
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_PACKAGE_NAME := PartnerSupportSampleTvInput
-LOCAL_MODULE_TAGS := optional
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
-    android-support-annotations \
-    live-channels-partner-support
-
-LOCAL_STATIC_ANDROID_LIBRARIES := \
-    android-support-compat \
-    android-support-core-ui \
-    android-support-v7-recyclerview \
-    android-support-v17-leanback \
-    androidx.tvprovider_tvprovider
-
-LOCAL_USE_AAPT2 := true
-
-LOCAL_PROGUARD_ENABLED := disabled
-# Overlay view related functionality requires system APIs.
-LOCAL_SDK_VERSION := system_current
-LOCAL_MIN_SDK_VERSION := 23  # M
-
-# Required for com.android.tv.permission.RECEIVE_INPUT_EVENT
-LOCAL_PRIVILEGED_MODULE := true
-
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-
-include $(BUILD_PACKAGE)
diff --git a/partner_support/samples/AndroidManifest.xml b/partner_support/samples/AndroidManifest.xml
index d91c603..65b2a3b 100644
--- a/partner_support/samples/AndroidManifest.xml
+++ b/partner_support/samples/AndroidManifest.xml
@@ -29,7 +29,7 @@
     <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="27" android:minSdkVersion="23"/>
+    <uses-sdk android:targetSdkVersion="28" 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,appComponentFactory"
diff --git a/partner_support/samples/src/com/example/partnersupportsampletvinput/ChannelScanFragment.java b/partner_support/samples/src/com/example/partnersupportsampletvinput/ChannelScanFragment.java
index ec7589c..d449bb5 100644
--- a/partner_support/samples/src/com/example/partnersupportsampletvinput/ChannelScanFragment.java
+++ b/partner_support/samples/src/com/example/partnersupportsampletvinput/ChannelScanFragment.java
@@ -23,10 +23,10 @@
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.support.annotation.NonNull;
-import android.support.v17.leanback.app.GuidedStepFragment;
-import android.support.v17.leanback.widget.GuidanceStylist;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
+import androidx.leanback.app.GuidedStepFragment;
+import androidx.leanback.widget.GuidanceStylist;
+import androidx.leanback.widget.GuidanceStylist.Guidance;
+import androidx.leanback.widget.GuidedAction;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
diff --git a/partner_support/samples/src/com/example/partnersupportsampletvinput/LineupSelectionFragment.java b/partner_support/samples/src/com/example/partnersupportsampletvinput/LineupSelectionFragment.java
index 8c3ca77..7486a98 100644
--- a/partner_support/samples/src/com/example/partnersupportsampletvinput/LineupSelectionFragment.java
+++ b/partner_support/samples/src/com/example/partnersupportsampletvinput/LineupSelectionFragment.java
@@ -24,21 +24,24 @@
 import android.os.Bundle;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
-import android.support.v17.leanback.app.GuidedStepFragment;
-import android.support.v17.leanback.widget.GuidanceStylist;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.Pair;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+
+import androidx.leanback.app.GuidedStepFragment;
+import androidx.leanback.widget.GuidanceStylist;
+import androidx.leanback.widget.GuidanceStylist.Guidance;
+import androidx.leanback.widget.GuidedAction;
+
 import com.google.android.tv.partner.support.EpgContract;
 import com.google.android.tv.partner.support.EpgInput;
 import com.google.android.tv.partner.support.EpgInputs;
 import com.google.android.tv.partner.support.Lineup;
 import com.google.android.tv.partner.support.Lineups;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -176,7 +179,7 @@
             List<Lineup> lineups, List<String> localChannels) {
         List<Pair<Lineup, Integer>> result = new ArrayList<>();
         for (Lineup lineup : lineups) {
-            result.add(new Pair<>(lineup, getMatchCount(lineup.getChannels(), localChannels)));
+            result.add(Pair.create(lineup, getMatchCount(lineup.getChannels(), localChannels)));
         }
         // sort in decreasing order
         Collections.sort(
diff --git a/partner_support/samples/src/com/example/partnersupportsampletvinput/LocationFragment.java b/partner_support/samples/src/com/example/partnersupportsampletvinput/LocationFragment.java
index 9492e7e..532ff9b 100644
--- a/partner_support/samples/src/com/example/partnersupportsampletvinput/LocationFragment.java
+++ b/partner_support/samples/src/com/example/partnersupportsampletvinput/LocationFragment.java
@@ -19,10 +19,10 @@
 import android.app.FragmentManager;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
-import android.support.v17.leanback.app.GuidedStepFragment;
-import android.support.v17.leanback.widget.GuidanceStylist;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
+import androidx.leanback.app.GuidedStepFragment;
+import androidx.leanback.widget.GuidanceStylist;
+import androidx.leanback.widget.GuidanceStylist.Guidance;
+import androidx.leanback.widget.GuidedAction;
 import java.util.List;
 
 /** Location Fragment for users to enter postal code */
diff --git a/partner_support/samples/src/com/example/partnersupportsampletvinput/ResultFragment.java b/partner_support/samples/src/com/example/partnersupportsampletvinput/ResultFragment.java
index a1c17ac..0c89318 100644
--- a/partner_support/samples/src/com/example/partnersupportsampletvinput/ResultFragment.java
+++ b/partner_support/samples/src/com/example/partnersupportsampletvinput/ResultFragment.java
@@ -21,10 +21,10 @@
 import android.media.tv.TvInputInfo;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
-import android.support.v17.leanback.app.GuidedStepFragment;
-import android.support.v17.leanback.widget.GuidanceStylist;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
+import androidx.leanback.app.GuidedStepFragment;
+import androidx.leanback.widget.GuidanceStylist;
+import androidx.leanback.widget.GuidanceStylist.Guidance;
+import androidx.leanback.widget.GuidedAction;
 import com.google.android.tv.partner.support.EpgContract;
 import java.util.List;
 
diff --git a/partner_support/samples/src/com/example/partnersupportsampletvinput/SampleTvInputSetupActivity.java b/partner_support/samples/src/com/example/partnersupportsampletvinput/SampleTvInputSetupActivity.java
index a0a7588..e11ebdf 100644
--- a/partner_support/samples/src/com/example/partnersupportsampletvinput/SampleTvInputSetupActivity.java
+++ b/partner_support/samples/src/com/example/partnersupportsampletvinput/SampleTvInputSetupActivity.java
@@ -18,7 +18,7 @@
 
 import android.app.Activity;
 import android.os.Bundle;
-import android.support.v17.leanback.app.GuidedStepFragment;
+import androidx.leanback.app.GuidedStepFragment;
 
 /** The setup activity for partner support sample TV input. */
 public class SampleTvInputSetupActivity extends Activity {
diff --git a/partner_support/samples/src/com/example/partnersupportsampletvinput/WelcomeFragment.java b/partner_support/samples/src/com/example/partnersupportsampletvinput/WelcomeFragment.java
index 286f34f..96632d3 100644
--- a/partner_support/samples/src/com/example/partnersupportsampletvinput/WelcomeFragment.java
+++ b/partner_support/samples/src/com/example/partnersupportsampletvinput/WelcomeFragment.java
@@ -19,10 +19,10 @@
 import android.app.FragmentManager;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
-import android.support.v17.leanback.app.GuidedStepFragment;
-import android.support.v17.leanback.widget.GuidanceStylist;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
+import androidx.leanback.app.GuidedStepFragment;
+import androidx.leanback.widget.GuidanceStylist;
+import androidx.leanback.widget.GuidanceStylist.Guidance;
+import androidx.leanback.widget.GuidedAction;
 import java.util.List;
 
 /** Welcome Fragment shows welcome information for users */
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 20b3542..c591c9f 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
@@ -20,7 +20,7 @@
 import com.google.auto.value.AutoValue;
 
 /**
- * Value class representing a TV Input that uses Live TV EPG.
+ * Value class representing a TV Input that uses TV app EPG.
  *
  * @see {@link EpgContract.EpgInputs}
  */
diff --git a/partner_support/src/com/google/android/tv/partner/support/TunerSetupUtils.java b/partner_support/src/com/google/android/tv/partner/support/TunerSetupUtils.java
index e25d583..d50db7d 100644
--- a/partner_support/src/com/google/android/tv/partner/support/TunerSetupUtils.java
+++ b/partner_support/src/com/google/android/tv/partner/support/TunerSetupUtils.java
@@ -20,6 +20,7 @@
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.Pair;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -39,7 +40,7 @@
         List<List<String>> parsedLocalChannels = parseChannelNumbers(localChannels);
         for (Lineup lineup : lineups) {
             result.add(
-                    new Pair<>(lineup, getMatchCount(lineup.getChannels(), parsedLocalChannels)));
+                    Pair.create(lineup, getMatchCount(lineup.getChannels(), parsedLocalChannels)));
         }
         // sort in decreasing order
         Collections.sort(
diff --git a/res/layout/details_overview.xml b/res/layout/details_overview.xml
index dbcf205..541f3fb 100644
--- a/res/layout/details_overview.xml
+++ b/res/layout/details_overview.xml
@@ -55,7 +55,7 @@
                 android:orientation="vertical" >
 
                 <!-- layout_marginStart and layout_marginEnd are overridden -->
-                <android.support.v17.leanback.widget.NonOverlappingFrameLayout
+                <androidx.leanback.widget.NonOverlappingFrameLayout
                     android:id="@+id/details_overview_description"
                     android:layout_width="match_parent"
                     android:layout_height="0dp"
@@ -69,7 +69,7 @@
 
                 <!-- horizontalSpacing is defined as @dimen/lb_details_overview_action_items_spacing
                      in newer versions of Leanback Library than LC uses. -->
-                <android.support.v17.leanback.widget.HorizontalGridView
+                <androidx.leanback.widget.HorizontalGridView
                     android:id="@+id/details_overview_actions"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
diff --git a/res/layout/dvr_details_description.xml b/res/layout/dvr_details_description.xml
index ee74952..e37d8b0 100644
--- a/res/layout/dvr_details_description.xml
+++ b/res/layout/dvr_details_description.xml
@@ -21,7 +21,7 @@
     android:layout_height="wrap_content">
 
     <!-- Top margins are set programatically -->
-    <android.support.v17.leanback.widget.ResizingTextView
+    <androidx.leanback.widget.ResizingTextView
         android:id="@+id/dvr_details_description_title"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
diff --git a/res/layout/item_list.xml b/res/layout/item_list.xml
index c06b29a..233aa41 100644
--- a/res/layout/item_list.xml
+++ b/res/layout/item_list.xml
@@ -33,7 +33,7 @@
          and compensate the same space with padding.
 
          The accurate layout height is set in MenuRowView.onBind(). -->
-    <android.support.v17.leanback.widget.HorizontalGridView
+    <androidx.leanback.widget.HorizontalGridView
         android:id="@+id/list_view"
         style="@style/menu_row_contents_view"
         android:clipChildren="false"
diff --git a/res/layout/option_fragment.xml b/res/layout/option_fragment.xml
index 4a4cbbd..50bf991 100644
--- a/res/layout/option_fragment.xml
+++ b/res/layout/option_fragment.xml
@@ -34,7 +34,7 @@
         android:textColor="@color/option_item_text_color"
         android:singleLine="true" />
 
-    <android.support.v17.leanback.widget.VerticalGridView
+    <androidx.leanback.widget.VerticalGridView
         android:id="@+id/side_panel_list"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
diff --git a/res/layout/pin_dialog.xml b/res/layout/pin_dialog.xml
index d40d70e..84807f2 100644
--- a/res/layout/pin_dialog.xml
+++ b/res/layout/pin_dialog.xml
@@ -55,8 +55,8 @@
             android:fontFamily="@string/font"
             android:singleLine="false" />
 
-        <com.android.tv.dialog.picker.PinPicker
-            android:id="@+id/pin_picker"
+        <com.android.tv.dialog.picker.TvPinPicker
+            android:id="@+id/tv_pin_picker"
             android:importantForAccessibility="yes"
             android:layout_width="match_parent"
             android:layout_height="154dp"
diff --git a/res/layout/priority_settings_action_item.xml b/res/layout/priority_settings_action_item.xml
index 0f51731..fc882d9 100644
--- a/res/layout/priority_settings_action_item.xml
+++ b/res/layout/priority_settings_action_item.xml
@@ -15,13 +15,13 @@
      limitations under the License.
 -->
 
-<android.support.v17.leanback.widget.GuidedActionItemContainer
+<androidx.leanback.widget.GuidedActionItemContainer
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     style="?attr/guidedActionItemContainerStyle"
     android:foreground="@null"
     android:outlineProvider="background">
-    <android.support.v17.leanback.widget.CheckableImageView
+    <androidx.leanback.widget.CheckableImageView
         android:id="@+id/guidedactions_item_checkmark"
         style="?attr/guidedActionItemCheckmarkStyle"
         tools:ignore="ContentDescription" />
@@ -30,16 +30,16 @@
         style="?attr/guidedActionItemIconStyle"
         tools:ignore="ContentDescription" />
 
-    <android.support.v17.leanback.widget.NonOverlappingLinearLayout
+    <androidx.leanback.widget.NonOverlappingLinearLayout
         android:id="@+id/guidedactions_item_content"
         style="?attr/guidedActionItemContentStyle" >
-        <android.support.v17.leanback.widget.GuidedActionEditText
+        <androidx.leanback.widget.GuidedActionEditText
             android:id="@+id/guidedactions_item_title"
             style="?attr/guidedActionItemTitleStyle" />
-        <android.support.v17.leanback.widget.GuidedActionEditText
+        <androidx.leanback.widget.GuidedActionEditText
             android:id="@+id/guidedactions_item_description"
             style="?attr/guidedActionItemDescriptionStyle" />
-    </android.support.v17.leanback.widget.NonOverlappingLinearLayout>
+    </androidx.leanback.widget.NonOverlappingLinearLayout>
 
     <ImageView
         android:id="@+id/guidedactions_item_chevron"
@@ -53,4 +53,4 @@
         android:adjustViewBounds="true"
         android:src="@drawable/ic_draggable_white"
         tools:ignore="ContentDescription" />
-</android.support.v17.leanback.widget.GuidedActionItemContainer>
\ No newline at end of file
+</androidx.leanback.widget.GuidedActionItemContainer>
\ No newline at end of file
diff --git a/res/layout/program_guide_side_panel.xml b/res/layout/program_guide_side_panel.xml
index 466b7fa..9c04feb 100644
--- a/res/layout/program_guide_side_panel.xml
+++ b/res/layout/program_guide_side_panel.xml
@@ -32,7 +32,7 @@
     android:elevation="@dimen/program_guide_side_panel_elevation"
     android:background="@color/program_guide_side_panel_background">
 
-    <android.support.v17.leanback.widget.SearchOrbView
+    <androidx.leanback.widget.SearchOrbView
         android:id="@+id/program_guide_side_panel_search_orb"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
@@ -41,7 +41,7 @@
         android:nextFocusDown="@+id/program_guide_side_panel_grid_view"
         android:visibility="gone" />
 
-    <android.support.v17.leanback.widget.VerticalGridView
+    <androidx.leanback.widget.VerticalGridView
         android:id="@id/program_guide_side_panel_grid_view"
         android:layout_width="@dimen/program_guide_side_panel_item_width"
         android:layout_height="match_parent"
diff --git a/res/values/arrays-custom.xml b/res/values/arrays-custom.xml
new file mode 100644
index 0000000..252d6f4
--- /dev/null
+++ b/res/values/arrays-custom.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<!-- These are resources that are expected to be different for OEM apps -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+    <!-- Keep the title and description arrays in sync -->
+
+    <!-- Titles in the onboarding page.  [CHAR LIMIT=100]-->
+    <string-array name="welcome_page_titles">
+        <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>
+    </string-array>
+
+
+    <!-- Descriptions in the onboarding page. [CHAR LIMIT=NONE] -->
+    <string-array name="welcome_page_descriptions">
+        <item>Watch content from your apps like watching channels on TV.</item>
+        <item>Browse content from your apps with a familiar guide and friendly interface,
+\njust like channels on TV.</item>
+        <item>Add more channels by installing apps that offer live channels.
+\nFind compatible apps in Google Play Store by using the link within the TV menu.</item>
+        <!-- Refer to @string/settings_channel_source_item_setup for "Channel sources" menu
+             and @string/options_item_settings for "Settings" menu. -->
+        <item>Set up your newly installed channel sources to customize your channel list.
+\nChoose the Channel sources within the Settings menu to get started.</item>
+    </string-array>
+</resources>
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
index 0604dd2..9ef028a 100644
--- a/res/values/arrays.xml
+++ b/res/values/arrays.xml
@@ -150,24 +150,4 @@
         <item>Tech/Science</item>
     </string-array>
 
-    <!-- Titles in the onboarding page. -->
-    <string-array name="welcome_page_titles">
-        <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>
-    </string-array>
-
-    <!-- Descriptions in the onboarding page. -->
-    <string-array name="welcome_page_descriptions">
-        <item>Watch content from your apps like watching channels on TV.</item>
-        <item>Browse content from your apps with a familiar guide and friendly interface,
-\njust like channels on TV.</item>
-        <item>Add more channels by installing apps that offer live channels.
-\nFind compatible apps in the online store by using the link within the TV menu.</item>
-        <!-- Refer to @string/settings_channel_source_item_setup for "Channel sources" menu
-             and @string/options_item_settings for "Settings" menu. -->
-        <item>Set up your newly installed channel sources to customize your channel list.
-\nChoose the Channel sources within the Settings menu to get started.</item>
-    </string-array>
 </resources>
diff --git a/res/values/rating_system_strings.xml b/res/values/rating_system_strings.xml
index 45c48d8..9922cb1 100644
--- a/res/values/rating_system_strings.xml
+++ b/res/values/rating_system_strings.xml
@@ -16,9 +16,10 @@
   -->
 
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <!-- Age based TV content rating strings used in DVB and ISDB.
-         For more info, please see STD-B10 in http://www.dibeg.org/techp/aribstd/aribstd.html (ISDB)
-         and Table 81 of DVB SI (EN 300 468 V1.14.1) in https://www.dvb.org/standards (DVB).-->
+    <!-- Age based TV content rating strings used in DVB, ISDB and DTMB.
+         For more info, please see STD-B10 in http://www.dibeg.org/techp/aribstd/aribstd.html (ISDB),
+         Table 81 of DVB SI (EN 300 468 V1.14.1) in https://www.dvb.org/standards (DVB)
+         and http://www.gb688.cn/bzgk/gb/newGbInfo?hcno=59E83CA701AEB4248E115BC043688FEC (DTMB).-->
     <string name="description_age_4">Recommended for ages 4 and over.</string>
     <string name="description_age_5">Recommended for ages 5 and over.</string>
     <string name="description_age_6">Recommended for ages 6 and over.</string>
@@ -109,6 +110,7 @@
     <!-- A TV content rating of DVB for adult [CHAR LIMIT=NONE] -->
     <string name="description_es_dvb_x">Recommended for adults.</string>
 
+
     <!-- TV content rating system strings for KR TV. These strings are from
          http://www.law.go.kr/admRulLsInfoP.do?admRulSeq=2000000118507 but they are translated
          from Korean to English. -->
@@ -159,4 +161,19 @@
     <string name="description_us_mv_pg13">Parents strongly cautioned. Some material may be inappropriate for pre-teenagers.</string>
     <string name="description_us_mv_r">Restricted, Contains some adult material. Parents are urged to learn more about the film before taking their young children with them.</string>
     <string name="description_us_mv_nc17">No one 17 and under admitted. Clearly adult. Children are not admitted.</string>
+
+    <!-- TV content rating system strings for NZ TV. These strings are from
+         https://bsa.govt.nz/images/03_BSA_FREE-TO-AIR-TV_CLASSIFICATIONS_DRAFT.pdf -->
+    <string name="description_nz_fta_tv_g" translatable="false">Programmes which exclude material likely to be unsuitable for children. Programmes may not necessarily be designed for child viewers but should not contain material likely to alarm or distress them.</string>
+    <string name="description_nz_fta_tv_pgr" translatable="false">Programmes containing material more suited for mature audiences but not necessarily unsuitable for child viewers when subject to the guidance of a parent or an adult.</string>
+    <string name="description_nz_fta_tv_ao" translatable="false">Programmes containing adult themes and directed primarily at mature audiences.</string>
+
+    <!-- TV content rating system strings for TH TV. These strings are from
+         https://broadcast.nbtc.go.th/home/ -->
+    <string name="description_th_tv_4" translatable="false">Suitable for audiences 3 to 5 years of age.</string>
+    <string name="description_th_tv_6" translatable="false">Suitable for audiences 6 to 12 years of age.</string>
+    <string name="description_th_tv_10" translatable="false">Suitable for all audiences.</string>
+    <string name="description_th_tv_13" translatable="false">Parental guidance suggested for viewers age below 13.</string>
+    <string name="description_th_tv_18" translatable="false">Parental guidance suggested for viewers age below 18.</string>
+    <string name="description_th_tv_19" translatable="false">Not suitable for children and teenagers.</string>
 </resources>
diff --git a/res/values/strings-custom.xml b/res/values/strings-custom.xml
index 22f7331..d9a7a26 100644
--- a/res/values/strings-custom.xml
+++ b/res/values/strings-custom.xml
@@ -14,9 +14,21 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<resources>
+
+
+<!-- These are resources that are expected to be different for OEM apps -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
 
     <!-- Name of application [CHAR LIMIT=NONE] -->
     <string name="app_name" translatable="false">Live TV</string>
+    <!-- Description for channel sources screen in onboarding. [CHAR LIMIT=NONE] -->
+    <string name="setup_sources_description">Live TV combines the experience of traditional TV channels with streaming channels provided by apps.
+\n\nGet started by setting up the channel sources already installed. Or browse Google Play Store for more apps that offer live channels.</string>
+    <!-- Description for channel sources screen in onboarding. [CHAR LIMIT=NONE] -->
+    <string name="setup_sources_description2"
+        meaning="Live TV version of setup_sources_description2"
+        ><xliff:g id="app_name">Live TV</xliff:g> combines the experience of traditional TV channels with streaming channels provided by apps.
+\n\nGet started by setting up the channel sources already installed. Or browse Google Play Store for more apps that offer live channels.</string>
 
-</resources>
\ No newline at end of file
+</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3682475..06694a9 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -28,16 +28,16 @@
 
     <!-- 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 <xliff:g id="app_name">Live TV</xliff:g>  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 <xliff:g id="app_name">Live TV</xliff:g>  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 <xliff:g id="app_name">Live TV</xliff:g>  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 <xliff:g id="app_name">Live TV</xliff:g>  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 />
@@ -388,13 +388,13 @@
     <!-- 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] -->
-    <string name="tvview_channel_locked_no_permission">To watch this channel, use the default Live TV app.</string>
+    <string name="tvview_channel_locked_no_permission">To watch this channel, use the default TV app.</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_no_permission">To watch this program, use the default Live TV app.</string>
+    <string name="tvview_content_locked_no_permission">To watch this program, use the default TV app.</string>
     <!-- Description on the locked screen when the current content is restricted by parental control. [CHAR LIMIT=NONE] -->
-    <string name="tvview_content_locked_unrated_no_permission">This program is unrated.\nTo watch this program, use the default Live TV app.</string>
+    <string name="tvview_content_locked_unrated_no_permission">This program is unrated.\nTo watch this program, use the default TV app.</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_no_permission">This program is rated <xliff:g id="rating" example="TV_MA">%1$s</xliff:g>.\nTo watch this program, use the default Live TV app.</string>
+    <string name="tvview_content_locked_format_no_permission">This program is rated <xliff:g id="rating" example="TV_MA">%1$s</xliff:g>.\nTo watch this program, use the default TV app.</string>
 
     <!-- Description on the locked screen when the rating of the current content is restricted by parental control. [CHAR LIMIT=NONE] -->
     <string name="shrunken_tvview_content_locked">Program is blocked</string>
@@ -479,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 <xliff:g id="app_name">Live TV</xliff:g>  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,
@@ -496,7 +496,7 @@
          (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"><xliff:g id="app_name">Live TV</xliff:g>  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 />
@@ -518,17 +518,17 @@
     <!-- Title of Recently watched dialog. It is used for debug purpose. -->
     <string name="recently_watched" translatable="false">Recently watched</string>
 
-    <!-- Title of DVR history dialog. -->
+    <!-- Title of DVR history dialog. [CHAR LIMIT=50] -->
     <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"><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"><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"><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"><xliff:g id="app_name">Live TV</xliff:g>  are updating recording schedules.</string>
+    <!-- Display name of DVR recording service's notification channel. [CHAR LIMIT=50] -->
+    <string name="dvr_notification_channel_name" ><xliff:g id="app_name">Live TV</xliff:g> DVR</string>
+    <!-- Content title of DVR recording service's notification. [CHAR LIMIT=50] -->
+    <string name="dvr_notification_content_title" ><xliff:g id="app_name">Live TV</xliff:g> DVR</string>
+    <!-- Content text of DVR recording service's notification during recording. [CHAR LIMIT=NONE] -->
+    <string name="dvr_notification_content_text_recording" ><xliff:g id="app_name">Live TV</xliff:g> are recording.</string>
+    <!-- Content text of DVR recording service's notification during updating schedules. [CHAR LIMIT=NONE] -->
+    <string name="dvr_notification_content_text_loading" ><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>
@@ -546,9 +546,6 @@
     <eat-comment />
     <!-- Text for the channel sources screen in onboarding. -->
     <string name="setup_sources_text">Set up your sources</string>
-    <!-- Description for channel sources screen in onboarding. -->
-    <string name="setup_sources_description">Live channels combines the experience of traditional TV channels with streaming channels provided by apps.
-\n\nGet started by setting up the channel sources already installed. Or browse the online store for more apps that offer live channels.</string>
 
     <!-- Menu item label to start DVR manager UI. -->
     <string name="channels_item_dvr">Recordings &amp; schedules</string>
@@ -693,8 +690,8 @@
         <item quantity="one">%1$d of %2$d episode is deleted</item>
         <item quantity="other">%1$d of %2$d episodes are deleted</item>
     </plurals>
-    <!-- Title of DVR series settings -->
-    <string name="dvr_series_settings_title" translatable="false">Recording settings</string>
+    <!-- Title of screen with settings for the recording of a TV Program or Series. [CHAR LIMIT=50] -->
+    <string name="dvr_series_settings_title">Recording settings</string>
     <!-- Item label to change priority of TV series for DVR -->
     <string name="dvr_series_settings_priority">Priority</string>
     <!-- Item description when the current series has the height proirty among scheduled
@@ -722,10 +719,13 @@
 
     <!-- DVR epg strings -->
     <eat-comment />
-    <string name="dvr_action_delete_schedule" translatable="false">Delete schedule</string>
-    <string name="dvr_action_record_program" translatable="false">Record program</string>
-    <!-- The action to forget DVR storage which is missing currently. invoke android internal storage settings activity. -->
-    <string name="dvr_action_error_storage_settings" translatable="false">Open storage settings</string>
+    <!--  Button text that deletes scheduled future recordings of a TV Program or Series. [CHAR LIMIT=50] -->
+    <string name="dvr_action_delete_schedule">Delete schedule</string>
+    <!--  Button text that records the selected program. [CHAR LIMIT=50] -->
+    <string name="dvr_action_record_program">Record program</string>
+    <!-- Button text that invokes android internal storage settings activity.
+         [CHAR LIMIT=50] -->
+    <string name="dvr_action_error_storage_settings">Open storage settings</string>
     <!-- The action to stop recording. [CHAR LIMIT=10] -->
     <string name="dvr_action_stop">Stop</string>
     <!-- The action to open the activity which shows all the schedules.[CHAR LIMIT=32] -->
@@ -767,8 +767,10 @@
     <!-- Dvr label in epg to indicate the recording of the program has been failed. [CHAR LIMIT=30] -->
     <string name="dvr_epg_program_recording_failed">Recording failed</string>
     <string name="dvr_epg_program_icon_text" translatable="false">DVR</string>
-    <string name="dvr_epg_channel_watch_conflict_dialog_title" translatable="false">Upcoming schedules</string>
-    <string name="dvr_epg_channel_watch_conflict_dialog_description" translatable="false">The programs will not be recorded if you keep watching this channel. Cancel the recordings, or current channel will be blocked when the recording starts.</string>
+    <!-- Title of a dialog box displayed when a previously scheduled recording requires an action.[CHAR LIMIT=50] -->
+    <string name="dvr_epg_channel_watch_conflict_dialog_title">Upcoming schedules</string>
+    <!-- Description in a dialog box displayed when a previously scheduled recording requires an action. [CHAR LIMIT=NONE] -->
+    <string name="dvr_epg_channel_watch_conflict_dialog_description">The program will not be recorded if you keep watching this channel. Cancel the recording, or the current channel will be blocked when the recording starts.</string>
     <!-- A popup message which informs that Live TV is reading program information. -->
     <string name="dvr_series_progress_message_reading_programs">Reading programs</string>
     <!-- Dialog action which let user view the recent recordings. -->
@@ -798,10 +800,10 @@
     <string name="dvr_error_no_free_space_description">This program will not be recorded because there is not enough storage. Try deleting some existing recordings.</string>
     <!-- Dialog title which will be shown when the current DVR storage is not accessible. -->
     <string name="dvr_error_missing_storage_title">Missing storage</string>
-    <!-- Dialog description which will be shown when the current DVR storage is not accessible. -->
-    <string name="dvr_error_missing_storage_description" translatable="false">Some of the storage used for recording is missing. Please connect the external drive you used before to re-enable recording. Alternately, you can forget the storage in the storage settings, if it\'s no longer available.</string>
-    <!-- The recording being requested to play is not existent in storage. It may be deleted. -->
-    <string name="dvr_toast_recording_deleted" translatable="false">The recording seems to be deleted.</string>
+    <!-- Dialog description which will be shown when the current DVR storage is not accessible. [CHAR LIMIT=NONE]  -->
+    <string name="dvr_error_missing_storage_description">Some of the storage used for recording is missing. Please connect the external drive you used before to re-enable recording. Alternately, you can forget the storage in the storage settings, if it\'s no longer available.</string>
+    <!-- A toast message displad to the user when the recording being requested to play is not found in storage. [CHAR LIMIT=NONE] -->
+    <string name="dvr_toast_recording_deleted">Recording not found.</string>
 
     <!-- DVR half sized dialogs -->
     <eat-comment />
@@ -908,7 +910,8 @@
 
     <!-- DVR channel banner strings -->
     <eat-comment />
-    <string name="dvr_recording_till_format" translatable="false">Recording till <xliff:g id="recordingEndTime" example="9:00pm">%1$s</xliff:g></string>
+    <!-- Text desplayed on screen to show that the current program is being recorded until the time listed. [CHAR LIMIT=30] -->
+    <string name="dvr_recording_till_format">Recording till <xliff:g id="recordingEndTime" example="9:00pm">%1$s</xliff:g></string>
 
     <!-- DVR schedule list strings -->
     <eat-comment/>
diff --git a/res/xml/tv_content_rating_systems.xml b/res/xml/tv_content_rating_systems.xml
index aad9d61..361393f 100644
--- a/res/xml/tv_content_rating_systems.xml
+++ b/res/xml/tv_content_rating_systems.xml
@@ -286,6 +286,170 @@
         </rating-order>
     </rating-system-definition>
 
+    <!-- TV content rating system for DTMB. See http://www.gb688.cn/bzgk/gb/newGbInfo?hcno=59E83CA701AEB4248E115BC043688FEC -->
+    <rating-system-definition android:name="DTMB"
+        android:country="CN">
+        <rating-definition android:name="DTMB_4"
+            android:title="4"
+            android:description="@string/description_age_4"
+            android:contentAgeHint="4" />
+        <rating-definition android:name="DTMB_5"
+            android:title="5"
+            android:description="@string/description_age_5"
+            android:contentAgeHint="5" />
+        <rating-definition android:name="DTMB_6"
+            android:title="6"
+            android:description="@string/description_age_6"
+            android:contentAgeHint="6" />
+        <rating-definition android:name="DTMB_7"
+            android:title="7"
+            android:description="@string/description_age_7"
+            android:contentAgeHint="7" />
+        <rating-definition android:name="DTMB_8"
+            android:title="8"
+            android:description="@string/description_age_8"
+            android:contentAgeHint="8" />
+        <rating-definition android:name="DTMB_9"
+            android:title="9"
+            android:description="@string/description_age_9"
+            android:contentAgeHint="9" />
+        <rating-definition android:name="DTMB_10"
+            android:title="10"
+            android:description="@string/description_age_10"
+            android:contentAgeHint="10" />
+        <rating-definition android:name="DTMB_11"
+            android:title="11"
+            android:description="@string/description_age_11"
+            android:contentAgeHint="11" />
+        <rating-definition android:name="DTMB_12"
+            android:title="12"
+            android:description="@string/description_age_12"
+            android:contentAgeHint="12" />
+        <rating-definition android:name="DTMB_13"
+            android:title="13"
+            android:description="@string/description_age_13"
+            android:contentAgeHint="13" />
+        <rating-definition android:name="DTMB_14"
+            android:title="14"
+            android:description="@string/description_age_14"
+            android:contentAgeHint="14" />
+        <rating-definition android:name="DTMB_15"
+            android:title="15"
+            android:description="@string/description_age_15"
+            android:contentAgeHint="15" />
+        <rating-definition android:name="DTMB_16"
+            android:title="16"
+            android:description="@string/description_age_16"
+            android:contentAgeHint="16" />
+        <rating-definition android:name="DTMB_17"
+            android:title="17"
+            android:description="@string/description_age_17"
+            android:contentAgeHint="17" />
+        <rating-definition android:name="DTMB_18"
+            android:title="18"
+            android:description="@string/description_age_18"
+            android:contentAgeHint="18" />
+        <rating-order>
+            <rating android:name="DTMB_4" />
+            <rating android:name="DTMB_5" />
+            <rating android:name="DTMB_6" />
+            <rating android:name="DTMB_7" />
+            <rating android:name="DTMB_8" />
+            <rating android:name="DTMB_9" />
+            <rating android:name="DTMB_10" />
+            <rating android:name="DTMB_11" />
+            <rating android:name="DTMB_12" />
+            <rating android:name="DTMB_13" />
+            <rating android:name="DTMB_14" />
+            <rating android:name="DTMB_15" />
+            <rating android:name="DTMB_16" />
+            <rating android:name="DTMB_17" />
+            <rating android:name="DTMB_18" />
+        </rating-order>
+    </rating-system-definition>
+
+    <!-- TV content rating system for DTMB. See http://www.gb688.cn/bzgk/gb/newGbInfo?hcno=59E83CA701AEB4248E115BC043688FEC -->
+    <rating-system-definition android:name="DTMB"
+        android:country="CN">
+        <rating-definition android:name="DTMB_4"
+            android:title="4"
+            android:description="@string/description_age_4"
+            android:contentAgeHint="4" />
+        <rating-definition android:name="DTMB_5"
+            android:title="5"
+            android:description="@string/description_age_5"
+            android:contentAgeHint="5" />
+        <rating-definition android:name="DTMB_6"
+            android:title="6"
+            android:description="@string/description_age_6"
+            android:contentAgeHint="6" />
+        <rating-definition android:name="DTMB_7"
+            android:title="7"
+            android:description="@string/description_age_7"
+            android:contentAgeHint="7" />
+        <rating-definition android:name="DTMB_8"
+            android:title="8"
+            android:description="@string/description_age_8"
+            android:contentAgeHint="8" />
+        <rating-definition android:name="DTMB_9"
+            android:title="9"
+            android:description="@string/description_age_9"
+            android:contentAgeHint="9" />
+        <rating-definition android:name="DTMB_10"
+            android:title="10"
+            android:description="@string/description_age_10"
+            android:contentAgeHint="10" />
+        <rating-definition android:name="DTMB_11"
+            android:title="11"
+            android:description="@string/description_age_11"
+            android:contentAgeHint="11" />
+        <rating-definition android:name="DTMB_12"
+            android:title="12"
+            android:description="@string/description_age_12"
+            android:contentAgeHint="12" />
+        <rating-definition android:name="DTMB_13"
+            android:title="13"
+            android:description="@string/description_age_13"
+            android:contentAgeHint="13" />
+        <rating-definition android:name="DTMB_14"
+            android:title="14"
+            android:description="@string/description_age_14"
+            android:contentAgeHint="14" />
+        <rating-definition android:name="DTMB_15"
+            android:title="15"
+            android:description="@string/description_age_15"
+            android:contentAgeHint="15" />
+        <rating-definition android:name="DTMB_16"
+            android:title="16"
+            android:description="@string/description_age_16"
+            android:contentAgeHint="16" />
+        <rating-definition android:name="DTMB_17"
+            android:title="17"
+            android:description="@string/description_age_17"
+            android:contentAgeHint="17" />
+        <rating-definition android:name="DTMB_18"
+            android:title="18"
+            android:description="@string/description_age_18"
+            android:contentAgeHint="18" />
+        <rating-order>
+            <rating android:name="DTMB_4" />
+            <rating android:name="DTMB_5" />
+            <rating android:name="DTMB_6" />
+            <rating android:name="DTMB_7" />
+            <rating android:name="DTMB_8" />
+            <rating android:name="DTMB_9" />
+            <rating android:name="DTMB_10" />
+            <rating android:name="DTMB_11" />
+            <rating android:name="DTMB_12" />
+            <rating android:name="DTMB_13" />
+            <rating android:name="DTMB_14" />
+            <rating android:name="DTMB_15" />
+            <rating android:name="DTMB_16" />
+            <rating android:name="DTMB_17" />
+            <rating android:name="DTMB_18" />
+        </rating-order>
+    </rating-system-definition>
+
     <!-- TV content rating system for DVB. See Table 81 of DVB SI (EN 300 468 V1.14.1) in https://www.dvb.org/standards -->
     <rating-system-definition android:name="DVB"
         android:country="AM, BG, CH, DE, DK, FI, GR, HU, ID, IE, IL, IS, MY, NL, NZ, PL, PT, RO, RU, RS, SI, TH, TR, TW, UA">
@@ -676,6 +840,28 @@
         </rating-order>
     </rating-system-definition>
 
+    <!-- TV content rating system for NZ. See http://www.freeviewnz.tv/ -->
+    <rating-system-definition android:name="NZ_TV"
+        android:country="NZ">
+        <rating-definition android:name="NZ_TV_G"
+            android:title="G"
+            android:description="@string/description_nz_fta_tv_g"
+            android:contentAgeHint="0" />
+        <rating-definition android:name="NZ_TV_PGR"
+            android:title="PGR"
+            android:description="@string/description_nz_fta_tv_pgr"
+            android:contentAgeHint="0" />
+        <rating-definition android:name="NZ_TV_AO"
+            android:title="AO"
+            android:description="@string/description_nz_fta_tv_ao"
+            android:contentAgeHint="0" />
+        <rating-order>
+            <rating android:name="NZ_TV_G" />
+            <rating android:name="NZ_TV_PGR" />
+            <rating android:name="NZ_TV_AO" />
+        </rating-order>
+    </rating-system-definition>
+
     <!-- TV content rating system for SG. See http://www.mda.gov.sg/RegulationsAndLicensing/ContentStandardsAndClassification/FilmsAndVideos/Pages/default.aspx -->
     <rating-system-definition android:name="SG_TV"
         android:country="SG">
@@ -713,6 +899,43 @@
         </rating-order>
     </rating-system-definition>
 
+    <!-- TV content rating system for TH. See https://broadcast.nbtc.go.th/home/ -->
+    <rating-system-definition android:name="TH_TV"
+        android:country="TH">
+        <rating-definition android:name="TH_TV_4"
+            android:title="4"
+            android:description="@string/description_th_tv_4"
+            android:contentAgeHint="4" />
+        <rating-definition android:name="TH_TV_6"
+            android:title="6"
+            android:description="@string/description_th_tv_6"
+            android:contentAgeHint="6" />
+        <rating-definition android:name="TH_TV_10"
+            android:title="10"
+            android:description="@string/description_th_tv_10"
+            android:contentAgeHint="10" />
+        <rating-definition android:name="TH_TV_13"
+            android:title="13"
+            android:description="@string/description_th_tv_13"
+            android:contentAgeHint="13" />
+        <rating-definition android:name="TH_TV_18"
+            android:title="18"
+            android:description="@string/description_th_tv_18"
+            android:contentAgeHint="18" />
+        <rating-definition android:name="TH_TV_19"
+            android:title="19"
+            android:description="@string/description_th_tv_19"
+            android:contentAgeHint="19" />
+        <rating-order>
+            <rating android:name="TH_TV_4" />
+            <rating android:name="TH_TV_6" />
+            <rating android:name="TH_TV_10" />
+            <rating android:name="TH_TV_13" />
+            <rating android:name="TH_TV_18" />
+            <rating android:name="TH_TV_19" />
+        </rating-order>
+    </rating-system-definition>
+
     <!-- TV content rating system for US. See http://www.tvguidelines.org/ratings.htm -->
     <rating-system-definition android:name="US_TV"
         android:country="US">
diff --git a/settings.gradle b/settings.gradle
index 6d5cb54..5916cb4 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -23,3 +23,5 @@
 include ':tuner'
 include ':SampleDvbTuner'
 project(":SampleDvbTuner").projectDir = file("tuner/SampleDvbTuner")
+include ':SampleNetworkTuner'
+project(":SampleNetworkTuner").projectDir = file("tuner/SampleNetworkTuner")
diff --git a/src/com/android/tv/LauncherActivity.java b/src/com/android/tv/LauncherActivity.java
index 679d612..ccc5600 100644
--- a/src/com/android/tv/LauncherActivity.java
+++ b/src/com/android/tv/LauncherActivity.java
@@ -27,10 +27,10 @@
  * An activity to launch a new activity.
  *
  * <p>In the case when {@link MainActivity} starts a new activity using {@link
- * Activity#startActivity} or {@link Activity#startActivityForResult}, Live TV app is
- * terminated if the new activity crashes. That's because the {@link android.app.ActivityManager}
- * terminates the activity which is just below the crashed activity in the activity stack. To avoid
- * this, we need to locate an additional activity between these activities in the activity stack.
+ * Activity#startActivity} or {@link Activity#startActivityForResult}, TV app is terminated if the
+ * new activity crashes. That's because the {@link android.app.ActivityManager} terminates the
+ * activity which is just below the crashed activity in the activity stack. To avoid this, we need
+ * to locate an additional activity between these activities in the activity stack.
  */
 public class LauncherActivity extends Activity {
     private static final String TAG = "LauncherActivity";
diff --git a/src/com/android/tv/MainActivity.java b/src/com/android/tv/MainActivity.java
index b4cf71d..141ce82 100644
--- a/src/com/android/tv/MainActivity.java
+++ b/src/com/android/tv/MainActivity.java
@@ -44,6 +44,7 @@
 import android.os.Handler;
 import android.os.Message;
 import android.os.PowerManager;
+import android.provider.BaseColumns;
 import android.provider.Settings;
 import android.support.annotation.IntDef;
 import android.support.annotation.NonNull;
@@ -65,6 +66,7 @@
 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;
@@ -78,6 +80,7 @@
 import com.android.tv.common.TvContentRatingCache;
 import com.android.tv.common.WeakHandler;
 import com.android.tv.common.compat.TvInputInfoCompat;
+import com.android.tv.common.dev.DeveloperPreferences;
 import com.android.tv.common.feature.CommonFeatures;
 import com.android.tv.common.memory.MemoryManageable;
 import com.android.tv.common.singletons.HasSingletons;
@@ -91,11 +94,13 @@
 import com.android.tv.data.ChannelDataManager;
 import com.android.tv.data.ChannelImpl;
 import com.android.tv.data.OnCurrentProgramUpdatedListener;
-import com.android.tv.data.Program;
 import com.android.tv.data.ProgramDataManager;
+import com.android.tv.data.ProgramImpl;
 import com.android.tv.data.StreamInfo;
 import com.android.tv.data.WatchedHistoryManager;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
+import com.android.tv.data.epg.EpgFetcher;
 import com.android.tv.dialog.HalfSizedDialogFragment;
 import com.android.tv.dialog.PinDialogFragment;
 import com.android.tv.dialog.PinDialogFragment.OnPinCheckedListener;
@@ -106,11 +111,12 @@
 import com.android.tv.dvr.ui.DvrStopRecordingFragment;
 import com.android.tv.dvr.ui.DvrUiHelper;
 import com.android.tv.features.TvFeatures;
+import com.android.tv.guide.ProgramItemView;
 import com.android.tv.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.PerformanceMonitorManagerFactory;
+import com.android.tv.perf.StartupMeasureFactory;
 import com.android.tv.receiver.AudioCapabilitiesReceiver;
 import com.android.tv.recommendation.ChannelPreviewUpdater;
 import com.android.tv.recommendation.NotificationService;
@@ -126,6 +132,7 @@
 import com.android.tv.ui.TunableTvView.BlockScreenType;
 import com.android.tv.ui.TunableTvView.OnTuneListener;
 import com.android.tv.ui.TvOverlayManager;
+import com.android.tv.ui.TvOverlayManagerFactory;
 import com.android.tv.ui.TvViewUiManager;
 import com.android.tv.ui.sidepanel.ClosedCaptionFragment;
 import com.android.tv.ui.sidepanel.CustomizeChannelListFragment;
@@ -135,6 +142,7 @@
 import com.android.tv.ui.sidepanel.SettingsFragment;
 import com.android.tv.ui.sidepanel.SideFragment;
 import com.android.tv.ui.sidepanel.parentalcontrols.ParentalControlsFragment;
+import com.android.tv.ui.sidepanel.parentalcontrols.RatingsFragment;
 import com.android.tv.util.AsyncDbTask;
 import com.android.tv.util.AsyncDbTask.DbExecutor;
 import com.android.tv.util.CaptionSettings;
@@ -150,9 +158,17 @@
 import com.android.tv.util.images.ImageCache;
 
 import com.google.common.base.Optional;
+
 import dagger.android.AndroidInjection;
+import dagger.android.AndroidInjector;
 import dagger.android.ContributesAndroidInjector;
+import dagger.android.DispatchingAndroidInjector;
+import dagger.android.HasAndroidInjector;
+
 import com.android.tv.common.flags.BackendKnobsFlags;
+import com.android.tv.common.flags.LegacyFlags;
+import com.android.tv.common.flags.StartupFlags;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayDeque;
@@ -163,15 +179,17 @@
 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. */
+/** The main activity for the TV app. */
 public class MainActivity extends Activity
         implements OnActionClickListener,
                 OnPinCheckedListener,
                 ChannelChanger,
-                HasSingletons<MySingletons> {
+                HasSingletons<MySingletons>,
+                HasAndroidInjector {
     private static final String TAG = "MainActivity";
     private static final boolean DEBUG = false;
     private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
@@ -258,10 +276,11 @@
     private static final long START_UP_TIMER_RESET_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(3);
 
     {
-        PerformanceMonitorManagerFactory.create().getStartupMeasure().onActivityInit();
+        StartupMeasureFactory.create().onActivityInit();
     }
 
     private final MySingletonsImpl mMySingletons = new MySingletonsImpl();
+    @Inject DispatchingAndroidInjector<Object> mAndroidInjector;
     @Inject @DbExecutor Executor mDbExecutor;
 
     private AccessibilityManager mAccessibilityManager;
@@ -278,8 +297,12 @@
     private DvrManager mDvrManager;
     private ConflictChecker mDvrConflictChecker;
     @Inject BackendKnobsFlags mBackendKnobs;
+    @Inject LegacyFlags mLegacyFlags;
+    @Inject StartupFlags mStartupFlags;
     @Inject SetupUtils mSetupUtils;
     @Inject Optional<BuiltInTunerManager> mOptionalBuiltInTunerManager;
+    @Inject AccountHelper mAccountHelper;
+    @Inject EpgFetcher mEpgFetcher;
 
     @VisibleForTesting protected TunableTvView mTvView;
     private View mContentView;
@@ -314,6 +337,7 @@
     private boolean mIsFilmModeSet;
     private float mDefaultRefreshRate;
 
+    @Inject TvOverlayManagerFactory mOverlayFactory;
     private TvOverlayManager mOverlayManager;
 
     // mIsCurrentChannelUnblockedByUser and mWasChannelUnblockedBeforeShrunkenByUser are used for
@@ -477,7 +501,6 @@
         AndroidInjection.inject(this);
         mAccessibilityManager =
                 (AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE);
-        TvSingletons tvSingletons = TvSingletons.getSingletons(this);
         DurationTimer startUpDebugTimer = Debug.getTimer(Debug.TAG_START_UP_TIMER);
         if (!startUpDebugTimer.isStarted()
                 || startUpDebugTimer.getDuration() > START_UP_TIMER_RESET_THRESHOLD_MS) {
@@ -496,6 +519,7 @@
             finishAndRemoveTask();
             return;
         }
+        mAccountHelper.init();
 
         TvSingletons tvApplication = (TvSingletons) getApplication();
         // In API 23, TvContract.isChannelUriForPassthroughInput is hidden.
@@ -514,8 +538,8 @@
             return;
         }
         setContentView(R.layout.activity_tv);
-        mTvView = (TunableTvView) findViewById(R.id.main_tunable_tv_view);
-        mTvView.initialize(mProgramDataManager, mTvInputManagerHelper);
+        mTvView = findViewById(R.id.main_tunable_tv_view);
+        mTvView.initialize(mProgramDataManager, mTvInputManagerHelper, mLegacyFlags);
         mTvView.setOnUnhandledInputEventListener(
                 new OnUnhandledInputEventListener() {
                     @Override
@@ -545,12 +569,13 @@
         String inputId = Utils.getLastWatchedTunerInputId(this);
         if (!isPassthroughInput
                 && inputId != null
+                && !mStartupFlags.warmupInputidBlacklist().getElementList().contains(inputId)
                 && channelId != Channel.INVALID_ID) {
             mTvView.warmUpInput(inputId, TvContract.buildChannelUri(channelId));
         }
 
         tvApplication.getMainActivityWrapper().onMainActivityCreated(this);
-        if (BuildConfig.ENG && SystemProperties.ALLOW_STRICT_MODE.getValue()) {
+        if (BuildConfig.ENG && DeveloperPreferences.ALLOW_STRICT_MODE.get(this)) {
             Toast.makeText(this, "Using Strict Mode for eng builds", Toast.LENGTH_SHORT).show();
         }
         mTracker = tvApplication.getTracker();
@@ -612,13 +637,10 @@
         }
         mTvViewUiManager =
                 new TvViewUiManager(
-                        this,
-                        mTvView,
-                        (FrameLayout) findViewById(android.R.id.content),
-                        mTvOptionsManager);
+                        this, mTvView, findViewById(android.R.id.content), mTvOptionsManager);
 
         mContentView = findViewById(android.R.id.content);
-        ViewGroup sceneContainer = (ViewGroup) findViewById(R.id.scene_container);
+        ViewGroup sceneContainer = findViewById(R.id.scene_container);
         ChannelBannerView channelBannerView =
                 (ChannelBannerView)
                         getLayoutInflater().inflate(R.layout.channel_banner, sceneContainer, false);
@@ -671,7 +693,7 @@
                 });
         mSearchFragment = new ProgramGuideSearchFragment();
         mOverlayManager =
-                new TvOverlayManager(
+                mOverlayFactory.create(
                         this,
                         mChannelTuner,
                         mTvView,
@@ -709,8 +731,6 @@
                 SendChannelStatusRunnable.startChannelStatusRecurringRunner(
                         this, mTracker, mChannelDataManager);
 
-        // To avoid not updating Rating systems when changing language.
-        mTvInputManagerHelper.getContentRatingsManager().update();
         if (CommonFeatures.DVR.isEnabled(this)
                 && TvFeatures.SHOW_UPCOMING_CONFLICT_DIALOG.isEnabled(this)) {
             mDvrConflictChecker = new ConflictChecker(this);
@@ -742,7 +762,7 @@
                 mChannelDataManager.reload();
                 mProgramDataManager.reload();
 
-                // Restart live channels.
+                // Restart TV app.
                 Intent intent = getIntent();
                 finish();
                 startActivity(intent);
@@ -830,7 +850,7 @@
                     .getTunerInputController()
                     .executeNetworkTunerDiscoveryAsyncTask(this);
         }
-        TvSingletons.getSingletons(this).getEpgFetcher().fetchImmediatelyIfNeeded();
+        mEpgFetcher.fetchImmediatelyIfNeeded();
     }
 
     @Override
@@ -1127,8 +1147,8 @@
             Toast.makeText(this, R.string.msg_no_setup_activity, Toast.LENGTH_SHORT).show();
             return;
         }
-        // Even though other app can handle the intent, the setup launched by Live channels
-        // should go through Live channels SetupPassthroughActivity.
+        // Even though other app can handle the intent, the setup launched by TV app
+        // should go through TV app SetupPassthroughActivity.
         intent.setComponent(new ComponentName(this, SetupPassthroughActivity.class));
         try {
             // Now we know that the user intends to set up this input. Grant permission for writing
@@ -1398,7 +1418,9 @@
 
     @Override
     public boolean dispatchKeyEvent(KeyEvent event) {
-        if (SystemProperties.LOG_KEYEVENT.getValue()) Log.d(TAG, "dispatchKeyEvent(" + event + ")");
+        if (DeveloperPreferences.LOG_KEYEVENT.get(this)) {
+            Log.d(TAG, "dispatchKeyEvent(" + event + ")");
+        }
         // If an activity is closed on a back key down event, back key down events with none zero
         // repeat count or a back key up event can be happened without the first back key down
         // event which should be ignored in this activity.
@@ -1503,7 +1525,7 @@
                 new AsyncQueryProgramTask(
                                 mDbExecutor,
                                 programUriFromIntent,
-                                Program.PROJECTION,
+                                ProgramImpl.PROJECTION,
                                 null,
                                 null,
                                 null,
@@ -1534,7 +1556,7 @@
                 String inputId = mInitChannelUri.getQueryParameter("input");
                 long channelId = Utils.getLastWatchedChannelIdForInput(this, inputId);
                 if (channelId == Channel.INVALID_ID) {
-                    String[] projection = {Channels._ID};
+                    String[] projection = {BaseColumns._ID};
                     long time = System.currentTimeMillis();
                     try (Cursor cursor =
                             getContentResolver().query(uri, projection, null, null, null)) {
@@ -1582,7 +1604,7 @@
         protected Program onQuery(Cursor c) {
             Program program = null;
             if (c != null && c.moveToNext()) {
-                program = Program.fromCursor(c);
+                program = ProgramImpl.fromCursor(c);
             }
             return program;
         }
@@ -1598,7 +1620,7 @@
                 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.PROGRAM, program.toParcelable());
                 intent.putExtra(DetailsActivity.INPUT_ID, channel.getInputId());
                 startActivity(intent);
             }
@@ -2077,7 +2099,7 @@
 
     @Override
     public boolean onKeyDown(int keyCode, KeyEvent event) {
-        if (SystemProperties.LOG_KEYEVENT.getValue()) {
+        if (DeveloperPreferences.LOG_KEYEVENT.get(this)) {
             Log.d(TAG, "onKeyDown(" + keyCode + ", " + event + ")");
         }
         switch (mOverlayManager.onKeyDown(keyCode, event)) {
@@ -2167,7 +2189,7 @@
          *  W debug: toggle screen size
          *  V KEYCODE_MEDIA_RECORD debug: record the current channel for 30 sec
          */
-        if (SystemProperties.LOG_KEYEVENT.getValue()) {
+        if (DeveloperPreferences.LOG_KEYEVENT.get(this)) {
             Log.d(TAG, "onKeyUp(" + keyCode + ", " + event + ")");
         }
         // If we are in the middle of channel change, finish it before showing overlays.
@@ -2265,7 +2287,7 @@
                     // Channel change is already done in the head of this method.
                     return true;
                 case KeyEvent.KEYCODE_S:
-                    if (!SystemProperties.USE_DEBUG_KEYS.getValue()) {
+                    if (!DeveloperPreferences.USE_DEBUG_KEYS.get(this)) {
                         break;
                     }
                     // fall through.
@@ -2273,7 +2295,7 @@
                     mOverlayManager.getSideFragmentManager().show(new ClosedCaptionFragment());
                     return true;
                 case KeyEvent.KEYCODE_A:
-                    if (!SystemProperties.USE_DEBUG_KEYS.getValue()) {
+                    if (!DeveloperPreferences.USE_DEBUG_KEYS.get(this)) {
                         break;
                     }
                     // fall through.
@@ -2339,7 +2361,7 @@
             // in case that TV isn't showing properly (e.g. no browsable channel)
             return true;
         }
-        if (SystemProperties.USE_DEBUG_KEYS.getValue() || BuildConfig.ENG) {
+        if (DeveloperPreferences.USE_DEBUG_KEYS.get(this) || BuildConfig.ENG) {
             switch (keyCode) {
                 case KeyEvent.KEYCODE_W:
                     mDebugNonFullSizeScreen = !mDebugNonFullSizeScreen;
@@ -2395,7 +2417,7 @@
 
     @Override
     public boolean onKeyLongPress(int keyCode, KeyEvent event) {
-        if (SystemProperties.LOG_KEYEVENT.getValue()) Log.d(TAG, "onKeyLongPress(" + event);
+        if (DeveloperPreferences.LOG_KEYEVENT.get(this)) Log.d(TAG, "onKeyLongPress(" + event);
         if (USE_BACK_KEY_LONG_PRESS) {
             // Treat the BACK key long press as the normal press since we changed the behavior in
             // onBackPressed().
@@ -2465,7 +2487,7 @@
     }
 
     private boolean dispatchKeyEventToSession(final KeyEvent event) {
-        if (SystemProperties.LOG_KEYEVENT.getValue()) {
+        if (DeveloperPreferences.LOG_KEYEVENT.get(this)) {
             Log.d(TAG, "dispatchKeyEventToSession(" + event + ")");
         }
         boolean handled = false;
@@ -2778,6 +2800,11 @@
         }
     }
 
+    @Override
+    public AndroidInjector<Object> androidInjector() {
+        return mAndroidInjector;
+    }
+
     private static class MainActivityHandler extends WeakHandler<MainActivity> {
         MainActivityHandler(MainActivity mainActivity) {
             super(mainActivity);
@@ -2823,11 +2850,8 @@
             Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.MyOnTuneListener.onTune");
             mChannel = channel;
             mWasUnderShrunkenTvView = wasUnderShrunkenTvView;
-
-            if (mBackendKnobs.enablePartialProgramFetch()) {
-                // Fetch complete projection of tuned channel.
-                mProgramDataManager.prefetchChannel(channel.getId());
-            }
+            // Fetch complete projection of tuned channel.
+            mProgramDataManager.onChannelTuned(channel.getId());
         }
 
         @Override
@@ -2982,5 +3006,14 @@
     public abstract static class Module {
         @ContributesAndroidInjector
         abstract MainActivity contributesMainActivityActivityInjector();
+
+        @ContributesAndroidInjector
+        abstract DeveloperOptionFragment contributesDeveloperOptionFragment();
+
+        @ContributesAndroidInjector
+        abstract RatingsFragment contributesRatingsFragment();
+
+        @ContributesAndroidInjector
+        abstract ProgramItemView contributesProgramItemView();
     }
 }
diff --git a/src/com/android/tv/MediaSessionWrapper.java b/src/com/android/tv/MediaSessionWrapper.java
index a647a06..df88639 100644
--- a/src/com/android/tv/MediaSessionWrapper.java
+++ b/src/com/android/tv/MediaSessionWrapper.java
@@ -34,8 +34,9 @@
 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.data.api.Program;
 import com.android.tv.util.Utils;
 import com.android.tv.util.images.ImageLoader;
 
diff --git a/src/com/android/tv/SetupPassthroughActivity.java b/src/com/android/tv/SetupPassthroughActivity.java
index 5185b12..25049f1 100644
--- a/src/com/android/tv/SetupPassthroughActivity.java
+++ b/src/com/android/tv/SetupPassthroughActivity.java
@@ -18,7 +18,7 @@
 
 import android.app.Activity;
 import android.content.ActivityNotFoundException;
-import android.content.Context;
+import android.content.ComponentName;
 import android.content.Intent;
 import android.media.tv.TvInputInfo;
 import android.os.Bundle;
@@ -26,6 +26,8 @@
 import android.os.Looper;
 import android.support.annotation.MainThread;
 import android.util.Log;
+
+import com.android.tv.common.CommonConstants;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.actions.InputSetupActionUtils;
 import com.android.tv.data.ChannelDataManager;
@@ -36,9 +38,16 @@
 import com.android.tv.util.SetupUtils;
 import com.android.tv.util.TvInputManagerHelper;
 import com.android.tv.util.Utils;
+
 import com.google.android.tv.partner.support.EpgContract;
+
+import dagger.android.AndroidInjection;
+import dagger.android.ContributesAndroidInjector;
+
 import java.util.concurrent.TimeUnit;
 
+import javax.inject.Inject;
+
 /**
  * An activity to launch a TV input setup activity.
  *
@@ -55,18 +64,20 @@
     private TvInputInfo mTvInputInfo;
     private Intent mActivityAfterCompletion;
     private boolean mEpgFetcherDuringScan;
-    private EpgInputWhiteList mEpgInputWhiteList;
+    @Inject EpgInputWhiteList mEpgInputWhiteList;
+    @Inject TvInputManagerHelper mInputManager;
+    @Inject SetupUtils mSetupUtils;
+    @Inject ChannelDataManager mChannelDataManager;
+    @Inject EpgFetcher mEpgFetcher;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         if (DEBUG) Log.d(TAG, "onCreate");
+        AndroidInjection.inject(this);
         super.onCreate(savedInstanceState);
-        TvSingletons tvSingletons = TvSingletons.getSingletons(this);
-        TvInputManagerHelper inputManager = tvSingletons.getTvInputManagerHelper();
         Intent intent = getIntent();
         String inputId = intent.getStringExtra(InputSetupActionUtils.EXTRA_INPUT_ID);
-        mTvInputInfo = inputManager.getTvInputInfo(inputId);
-        mEpgInputWhiteList = new EpgInputWhiteList(tvSingletons.getCloudEpgFlags());
+        mTvInputInfo = mInputManager.getTvInputInfo(inputId);
         mActivityAfterCompletion = InputSetupActionUtils.getExtraActivityAfter(intent);
         boolean needToFetchEpg =
                 mTvInputInfo != null && Utils.isInternalTvInput(this, mTvInputInfo.getId());
@@ -106,6 +117,17 @@
             InputSetupActionUtils.removeSetupIntent(extras);
             setupIntent.putExtras(extras);
             try {
+                ComponentName callingActivity = getCallingActivity();
+                if (callingActivity != null
+                        && !callingActivity.getPackageName().equals(CommonConstants.BASE_PACKAGE)) {
+                    Log.w(
+                            TAG,
+                            "Calling activity "
+                                    + callingActivity.getPackageName()
+                                    + " is not trusted. Not forwarding intent.");
+                    finish();
+                    return;
+                }
                 startActivityForResult(setupIntent, REQUEST_START_SETUP_ACTIVITY);
             } catch (ActivityNotFoundException e) {
                 Log.e(TAG, "Can't find activity: " + setupIntent.getComponent());
@@ -114,10 +136,10 @@
             }
             if (needToFetchEpg) {
                 if (sScanTimeoutMonitor == null) {
-                    sScanTimeoutMonitor = new ScanTimeoutMonitor(this);
+                    sScanTimeoutMonitor = new ScanTimeoutMonitor(mEpgFetcher, mChannelDataManager);
                 }
                 sScanTimeoutMonitor.startMonitoring();
-                TvSingletons.getSingletons(this).getEpgFetcher().onChannelScanStarted();
+                mEpgFetcher.onChannelScanStarted();
             }
         }
     }
@@ -133,15 +155,25 @@
         boolean setupComplete =
                 requestCode == REQUEST_START_SETUP_ACTIVITY && resultCode == Activity.RESULT_OK;
         // Tells EpgFetcher that channel source setup is finished.
-        EpgFetcher epgFetcher = TvSingletons.getSingletons(this).getEpgFetcher();
+
         if (mEpgFetcherDuringScan) {
-            epgFetcher.onChannelScanFinished();
+            mEpgFetcher.onChannelScanFinished();
         }
         if (!setupComplete) {
             setResult(resultCode, data);
             finish();
             return;
         }
+        if (TvFeatures.CLOUD_EPG_FOR_3RD_PARTY.isEnabled(this)
+                && data != null
+                && data.getBooleanExtra(EpgContract.EXTRA_USE_CLOUD_EPG, false)) {
+            if (DEBUG) Log.d(TAG, "extra " + data.getExtras());
+            String inputId = data.getStringExtra(TvInputInfo.EXTRA_INPUT_ID);
+            if (mEpgInputWhiteList.isInputWhiteListed(inputId)) {
+                mEpgFetcher.fetchImmediately();
+            }
+        }
+
         if (mTvInputInfo == null) {
             Log.w(
                     TAG,
@@ -152,21 +184,19 @@
             finish();
             return;
         }
-        TvSingletons.getSingletons(this)
-                .getSetupUtils()
-                .onTvInputSetupFinished(
-                        mTvInputInfo.getId(),
-                        () -> {
-                            if (mActivityAfterCompletion != null) {
-                                try {
-                                    startActivity(mActivityAfterCompletion);
-                                } catch (ActivityNotFoundException e) {
-                                    Log.w(TAG, "Activity launch failed", e);
-                                }
-                            }
-                            setResult(resultCode, data);
-                            finish();
-                        });
+        mSetupUtils.onTvInputSetupFinished(
+                mTvInputInfo.getId(),
+                () -> {
+                    if (mActivityAfterCompletion != null) {
+                        try {
+                            startActivity(mActivityAfterCompletion);
+                        } catch (ActivityNotFoundException e) {
+                            Log.w(TAG, "Activity launch failed", e);
+                        }
+                    }
+                    setResult(resultCode, data);
+                    finish();
+                });
     }
 
     /**
@@ -179,7 +209,7 @@
         // Set timeout long enough. The message in Sony TV says the scanning takes about 30 minutes.
         private static final long SCAN_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(30);
 
-        private final Context mContext;
+        private final EpgFetcher mEpgFetcher;
         private final ChannelDataManager mChannelDataManager;
         private final Handler mHandler = new Handler(Looper.getMainLooper());
         private final Runnable mScanTimeoutRunnable =
@@ -207,9 +237,9 @@
                 };
         private boolean mStarted;
 
-        private ScanTimeoutMonitor(Context context) {
-            mContext = context.getApplicationContext();
-            mChannelDataManager = TvSingletons.getSingletons(context).getChannelDataManager();
+        private ScanTimeoutMonitor(EpgFetcher epgFetcher, ChannelDataManager mChannelDataManager) {
+            mEpgFetcher = epgFetcher;
+            this.mChannelDataManager = mChannelDataManager;
         }
 
         private void startMonitoring() {
@@ -237,7 +267,14 @@
 
         private void onScanTimedOut() {
             stopMonitoring();
-            TvSingletons.getSingletons(mContext).getEpgFetcher().onChannelScanFinished();
+            mEpgFetcher.onChannelScanFinished();
         }
     }
+
+    /** Exports {@link MainActivity} for Dagger codegen to create the appropriate injector. */
+    @dagger.Module
+    public abstract static class Module {
+        @ContributesAndroidInjector
+        abstract SetupPassthroughActivity contributesSetupPassthroughActivity();
+    }
 }
diff --git a/src/com/android/tv/TimeShiftManager.java b/src/com/android/tv/TimeShiftManager.java
index 779e8df..f08b5e8 100644
--- a/src/com/android/tv/TimeShiftManager.java
+++ b/src/com/android/tv/TimeShiftManager.java
@@ -26,18 +26,21 @@
 import android.support.annotation.VisibleForTesting;
 import android.util.Log;
 import android.util.Range;
+
 import com.android.tv.analytics.Tracker;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.WeakHandler;
 import com.android.tv.data.OnCurrentProgramUpdatedListener;
-import com.android.tv.data.Program;
 import com.android.tv.data.ProgramDataManager;
+import com.android.tv.data.ProgramImpl;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 import com.android.tv.ui.TunableTvView;
 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;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
@@ -50,7 +53,7 @@
 import java.util.concurrent.TimeUnit;
 
 /**
- * A class which manages the time shift feature in Live TV. It consists of two parts. {@link
+ * A class which manages the time shift feature in TV app. It consists of two parts. {@link
  * PlayController} controls the playback such as play/pause, rewind and fast-forward using {@link
  * TunableTvView} which communicates with TvInputService through {@link
  * android.media.tv.TvInputService.Session}. {@link ProgramManager} loads programs of the current
@@ -144,8 +147,8 @@
             DISABLE_ACTION_THRESHOLD + 3 * REQUEST_CURRENT_POSITION_INTERVAL;
     /**
      * The current position sent from TIS can not be exactly the same as the current system time due
-     * to the elapsed time to pass the message from TIS to Live TV. So the boundary threshold
-     * is necessary. The same goes for the recording start time. It's the same {@link
+     * to the elapsed time to pass the message from TIS to TV app. So the boundary threshold is
+     * necessary. The same goes for the recording start time. It's the same {@link
      * #REQUEST_CURRENT_POSITION_INTERVAL}.
      */
     private static final long RECORDING_BOUNDARY_THRESHOLD = REQUEST_CURRENT_POSITION_INTERVAL;
@@ -619,8 +622,8 @@
                                     < mAvailablityChangedTimeMs - ALLOWED_START_TIME_OFFSET) {
                                 Log.e(
                                         TAG,
-                                        "The start time is too earlier than the time of availability: {"
-                                                + "startTime: "
+                                        "The start time is too earlier than the time of"
+                                                + " availability: {startTime: "
                                                 + recordStartTimeMs
                                                 + ", availability: "
                                                 + mAvailablityChangedTimeMs);
@@ -632,9 +635,9 @@
                                 // clock,, use system's current time instead.
                                 Log.e(
                                         TAG,
-                                        "The start time should not be earlier than the current time, "
-                                                + "reset the start time to the system's current time: {"
-                                                + "startTime: "
+                                        "The start time should not be earlier than the current"
+                                            + " time, reset the start time to the system's current"
+                                            + " time: {startTime: "
                                                 + recordStartTimeMs
                                                 + ", current time: "
                                                 + System.currentTimeMillis());
@@ -1103,7 +1106,7 @@
             long end = Utils.ceilTime(startTimeMs, MAX_DUMMY_PROGRAM_DURATION);
             while (end < endTimeMs) {
                 programs.add(
-                        new Program.Builder()
+                        new ProgramImpl.Builder()
                                 .setStartTimeUtcMillis(start)
                                 .setEndTimeUtcMillis(end)
                                 .build());
@@ -1111,7 +1114,7 @@
                 end += MAX_DUMMY_PROGRAM_DURATION;
             }
             programs.add(
-                    new Program.Builder()
+                    new ProgramImpl.Builder()
                             .setStartTimeUtcMillis(start)
                             .setEndTimeUtcMillis(endTimeMs)
                             .build());
diff --git a/src/com/android/tv/TvApplication.java b/src/com/android/tv/TvApplication.java
index 5f25a24..bc8226c 100644
--- a/src/com/android/tv/TvApplication.java
+++ b/src/com/android/tv/TvApplication.java
@@ -35,20 +35,19 @@
 import android.util.Log;
 import android.view.KeyEvent;
 import android.widget.Toast;
+
 import com.android.tv.common.BaseApplication;
 import com.android.tv.common.feature.CommonFeatures;
 import com.android.tv.common.recording.RecordingStorageStatusManager;
 import com.android.tv.common.ui.setup.animation.SetupAnimationHelper;
-import com.android.tv.common.util.Clock;
 import com.android.tv.common.util.Debug;
 import com.android.tv.common.util.SharedPreferencesUtils;
 import com.android.tv.data.ChannelDataManager;
 import com.android.tv.data.PreviewDataManager;
 import com.android.tv.data.ProgramDataManager;
 import com.android.tv.data.epg.EpgFetcher;
-import com.android.tv.data.epg.EpgFetcherImpl;
+import com.android.tv.data.epg.EpgReader;
 import com.android.tv.dvr.DvrDataManager;
-import com.android.tv.dvr.DvrDataManagerImpl;
 import com.android.tv.dvr.DvrManager;
 import com.android.tv.dvr.DvrScheduleManager;
 import com.android.tv.dvr.DvrStorageStatusManager;
@@ -56,8 +55,9 @@
 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.perf.PerformanceMonitor;
+import com.android.tv.perf.StartupMeasure;
+import com.android.tv.perf.StartupMeasureFactory;
 import com.android.tv.recommendation.ChannelPreviewUpdater;
 import com.android.tv.recommendation.RecordedProgramPreviewUpdater;
 import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
@@ -66,27 +66,30 @@
 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 com.android.tv.common.flags.CloudEpgFlags;
+import com.android.tv.common.flags.LegacyFlags;
+
 import java.util.List;
 import java.util.concurrent.Executor;
+
 import javax.inject.Inject;
 
 /**
- * Live TV application.
+ * TV application.
  *
  * <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();
+    protected static final StartupMeasure STARTUP_MEASURE = StartupMeasureFactory.create();
     private static final String TAG = "TvApplication";
     private static final boolean DEBUG = false;
 
-    /** Namespace for LiveChannels configs. LiveChannels configs are kept in piper. */
-    public static final String CONFIGNS_P4 = "configns:p4";
-
     /**
      * Broadcast Action: The user has updated LC to a new version that supports tuner input. {@link
      * TunerInputController} will receive this intent to check the existence of tuner input when the
@@ -102,12 +105,12 @@
     private final MainActivityWrapper mMainActivityWrapper = new MainActivityWrapper();
 
     private SelectInputActivity mSelectInputActivity;
-    private ChannelDataManager mChannelDataManager;
+    @Inject Lazy<ChannelDataManager> mChannelDataManager;
     private volatile ProgramDataManager mProgramDataManager;
     private PreviewDataManager mPreviewDataManager;
     private DvrManager mDvrManager;
     private DvrScheduleManager mDvrScheduleManager;
-    private DvrDataManager mDvrDataManager;
+    @Inject Lazy<DvrDataManager> mDvrDataManager;
     private DvrWatchedPositionManager mDvrWatchedPositionManager;
     private RecordingScheduler mRecordingScheduler;
     private RecordingStorageStatusManager mDvrStorageStatusManager;
@@ -117,11 +120,16 @@
     private Boolean mRunningInMainProcess;
     @Inject Lazy<TvInputManagerHelper> mLazyTvInputManagerHelper;
     private boolean mStarted;
-    private EpgFetcher mEpgFetcher;
+    @Inject EpgFetcher mEpgFetcher;
 
     @Inject Optional<BuiltInTunerManager> mOptionalBuiltInTunerManager;
     @Inject SetupUtils mSetupUtils;
     @Inject @DbExecutor Executor mDbExecutor;
+    @Inject Lazy<EpgReader> mEpgReader;
+    @Inject BuildType mBuildType;
+    @Inject CloudEpgFlags mCloudEpgFlags;
+    @Inject LegacyFlags mLegacyFlags;
+    @Inject PerformanceMonitor mPerformanceMonitor;
 
     @Override
     public void onCreate() {
@@ -132,6 +140,8 @@
             throw new IllegalStateException(msg);
         }
         super.onCreate();
+        mPerformanceMonitor.startMemoryMonitor();
+        mPerformanceMonitor.startCrashMonitor();
         SharedPreferencesUtils.initialize(
                 this,
                 () -> {
@@ -146,16 +156,15 @@
             Log.w(TAG, "Unable to find package '" + getPackageName() + "'.", e);
             mVersionName = "";
         }
-        Log.i(TAG, "Starting Live TV " + getVersionName());
+        Log.i(TAG, "Starting TV app " + getVersionName());
 
         // In SetupFragment, transitions are set in the constructor. Because the fragment can be
         // created in Activity.onCreate() by the framework, SetupAnimationHelper should be
         // initialized here before Activity.onCreate() is called.
-        mEpgFetcher = EpgFetcherImpl.create(this);
         SetupAnimationHelper.initialize(this);
         getTvInputManagerHelper();
 
-        Log.i(TAG, "Started Live TV " + mVersionName);
+        Log.i(TAG, "Started TV app " + mVersionName);
         Debug.getTimer(Debug.TAG_START_UP_TIMER).log("finish TvApplication.onCreate");
     }
 
@@ -210,8 +219,10 @@
             mEpgFetcher.startRoutineService();
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                 ChannelPreviewUpdater.getInstance(this).startRoutineService();
-                RecordedProgramPreviewUpdater.getInstance(this)
-                        .updatePreviewDataForRecordedPrograms();
+                if (CommonFeatures.DVR.isEnabled(this)) {
+                    RecordedProgramPreviewUpdater.getInstance(this)
+                            .updatePreviewDataForRecordedPrograms();
+                }
             }
         }
         Debug.getTimer(Debug.TAG_START_UP_TIMER).log("finish TvApplication.start");
@@ -237,11 +248,6 @@
     }
 
     @Override
-    public EpgFetcher getEpgFetcher() {
-        return mEpgFetcher;
-    }
-
-    @Override
     public synchronized SetupUtils getSetupUtils() {
         return mSetupUtils;
     }
@@ -286,16 +292,7 @@
     /** Returns {@link ChannelDataManager}. */
     @Override
     public ChannelDataManager getChannelDataManager() {
-        if (mChannelDataManager == null) {
-            mChannelDataManager = new ChannelDataManager(this, getTvInputManagerHelper());
-            mChannelDataManager.start();
-        }
-        return mChannelDataManager;
-    }
-
-    @Override
-    public boolean isChannelDataManagerLoadFinished() {
-        return mChannelDataManager != null && mChannelDataManager.isDbLoadFinished();
+        return mChannelDataManager.get();
     }
 
     /** Returns {@link ProgramDataManager}. */
@@ -314,11 +311,6 @@
         return mProgramDataManager;
     }
 
-    @Override
-    public boolean isProgramDataManagerCurrentProgramsLoadFinished() {
-        return mProgramDataManager != null && mProgramDataManager.isCurrentProgramsLoadFinished();
-    }
-
     /** Returns {@link PreviewDataManager}. */
     @TargetApi(Build.VERSION_CODES.O)
     @Override
@@ -334,12 +326,7 @@
     @TargetApi(Build.VERSION_CODES.N)
     @Override
     public DvrDataManager getDvrDataManager() {
-        if (mDvrDataManager == null) {
-            DvrDataManagerImpl dvrDataManager = new DvrDataManagerImpl(this, Clock.SYSTEM);
-            mDvrDataManager = dvrDataManager;
-            dvrDataManager.start();
-        }
-        return mDvrDataManager;
+        return mDvrDataManager.get();
     }
 
     @Override
@@ -351,6 +338,11 @@
         return mDvrStorageStatusManager;
     }
 
+    @Override
+    public PerformanceMonitor getPerformanceMonitor() {
+        return mPerformanceMonitor;
+    }
+
     /** Returns the main activity information. */
     @Override
     public MainActivityWrapper getMainActivityWrapper() {
@@ -450,7 +442,7 @@
     }
 
     /**
-     * Returns the version name of the live channels.
+     * Returns the version name of the TV app.
      *
      * @see PackageInfo#versionName
      */
@@ -489,6 +481,37 @@
             Optional<String> optionalEmbeddedTunerInputId =
                     mOptionalBuiltInTunerManager.transform(
                             BuiltInTunerManager::getEmbeddedTunerInputId);
+            // If there is only play movies trailer input, we don't handle input count change.
+            final String playMoviesInputIdPrefix = "com.google.android.videos/";
+            int tunerInputCount = 0;
+            boolean hasPlayMoviesInput = false;
+            for (TvInputInfo input : inputs) {
+                if (calledByTunerServiceChanged
+                        && !tunerServiceEnabled
+                        && optionalEmbeddedTunerInputId.isPresent()
+                        && optionalEmbeddedTunerInputId.get().equals(input.getId())) {
+                    continue;
+                }
+                if (input.getType() == TvInputInfo.TYPE_TUNER) {
+                    if (DEBUG) Log.d(TAG, "Tuner input: " + input.getId());
+                    ++tunerInputCount;
+                    if (input.getId().startsWith(playMoviesInputIdPrefix)) {
+                        hasPlayMoviesInput = true;
+                    }
+                }
+            }
+            if (DEBUG) {
+                Log.d(
+                        TAG,
+                        "Input count: "
+                                + tunerInputCount
+                                + " hasPlayMoviesChannel: "
+                                + hasPlayMoviesInput);
+            }
+            if (tunerInputCount == 1 && hasPlayMoviesInput) {
+                if (DEBUG) Log.d(TAG, "There is only play movies input");
+                skipTunerInputCheck = true;
+            }
             // Enable the TvActivity only if there is at least one tuner type input.
             if (!skipTunerInputCheck) {
                 for (TvInputInfo input : inputs) {
@@ -515,13 +538,24 @@
         if (packageManager.getComponentEnabledSetting(name) != newState) {
             packageManager.setComponentEnabledSetting(
                     name, newState, dontKillApp ? PackageManager.DONT_KILL_APP : 0);
-            Log.i(TAG, (enable ? "Un-hide" : "Hide") + " Live TV.");
+            Log.i(TAG, (enable ? "Un-hide" : "Hide") + " TV app.");
         }
         mSetupUtils.onInputListUpdated(inputManager);
     }
 
     @Override
+    @DbExecutor
     public Executor getDbExecutor() {
         return mDbExecutor;
     }
+
+    @Override
+    public Lazy<EpgReader> providesEpgReader() {
+        return mEpgReader;
+    }
+
+    @Override
+    public BuildType getBuildType() {
+        return mBuildType;
+    }
 }
diff --git a/src/com/android/tv/TvSingletons.java b/src/com/android/tv/TvSingletons.java
index 20edf3d..9e4f462 100644
--- a/src/com/android/tv/TvSingletons.java
+++ b/src/com/android/tv/TvSingletons.java
@@ -17,16 +17,15 @@
 package com.android.tv;
 
 import android.content.Context;
+
 import com.android.tv.analytics.Analytics;
 import com.android.tv.analytics.Tracker;
 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;
-import com.android.tv.data.epg.EpgFetcher;
 import com.android.tv.data.epg.EpgReader;
 import com.android.tv.dvr.DvrDataManager;
 import com.android.tv.dvr.DvrManager;
@@ -37,14 +36,27 @@
 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 dagger.Lazy;
+
 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, HasBuiltInTunerManager, HasUiFlags {
 
+    /*
+     * Do not add any new methods here.
+     *
+     * To move a getter to Injection.
+     *  1. Make a type injectable @Singleton.
+     *  2. Mark the getter here as deprecated.
+     *  3. Lazily inject the object in TvApplication.
+     *  4. Move easy usages of getters to injection instead.
+     *  5. Delete the method when all usages are migrated.
+     */
+
     /**
      * Returns the @{@link TvSingletons} using the application context.
      *
@@ -62,24 +74,14 @@
     @Deprecated
     ChannelDataManager getChannelDataManager();
 
-    /**
-     * Checks if the {@link ChannelDataManager} instance has been created and all the channels has
-     * been loaded.
-     */
-    boolean isChannelDataManagerLoadFinished();
-
     /** @deprecated use injection instead. */
     @Deprecated
     ProgramDataManager getProgramDataManager();
 
-    /**
-     * Checks if the {@link ProgramDataManager} instance has been created and the current programs
-     * for all the channels has been loaded.
-     */
-    boolean isProgramDataManagerCurrentProgramsLoadFinished();
-
     PreviewDataManager getPreviewDataManager();
 
+    /** @deprecated use injection instead. */
+    @Deprecated
     DvrDataManager getDvrDataManager();
 
     DvrScheduleManager getDvrScheduleManager();
@@ -88,6 +90,8 @@
 
     RecordingScheduler getRecordingScheduler();
 
+    /** @deprecated use injection instead. */
+    @Deprecated
     DvrWatchedPositionManager getDvrWatchedPositionManager();
 
     InputSessionManager getInputSessionManager();
@@ -96,26 +100,22 @@
 
     MainActivityWrapper getMainActivityWrapper();
 
-    AccountHelper getAccountHelper();
-
     boolean isRunningInMainProcess();
 
+    /** @deprecated use injection instead. */
+    @Deprecated
     PerformanceMonitor getPerformanceMonitor();
 
     /** @deprecated use injection instead. */
     @Deprecated
     TvInputManagerHelper getTvInputManagerHelper();
 
-    Provider<EpgReader> providesEpgReader();
-
-    EpgFetcher getEpgFetcher();
+    Lazy<EpgReader> providesEpgReader();
 
     /** @deprecated use injection instead. */
     @Deprecated
     SetupUtils getSetupUtils();
 
-    ExperimentLoader getExperimentLoader();
-
     /** @deprecated use injection instead. */
     @Deprecated
     Executor getDbExecutor();
diff --git a/src/com/android/tv/app/LiveTvApplication.java b/src/com/android/tv/app/LiveTvApplication.java
index 38e85e4..8090653 100644
--- a/src/com/android/tv/app/LiveTvApplication.java
+++ b/src/com/android/tv/app/LiveTvApplication.java
@@ -22,50 +22,34 @@
 import com.android.tv.analytics.StubAnalytics;
 import com.android.tv.analytics.Tracker;
 import com.android.tv.common.dagger.ApplicationModule;
-import com.android.tv.common.experiments.ExperimentLoader;
 import com.android.tv.common.flags.impl.DefaultBackendKnobsFlags;
 import com.android.tv.common.flags.impl.DefaultCloudEpgFlags;
-import com.android.tv.common.flags.impl.DefaultConcurrentDvrPlaybackFlags;
 import com.android.tv.common.flags.impl.DefaultUiFlags;
 import com.android.tv.common.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.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;
+
+import javax.inject.Inject;
 
 /** The top level application for Live TV. */
 public class LiveTvApplication extends TvApplication implements HasSingletons<TvSingletons> {
 
     static {
-        PERFORMANCE_MONITOR_MANAGER.getStartupMeasure().onAppClassLoaded();
+        STARTUP_MEASURE.onAppClassLoaded();
     }
 
-    private final Provider<EpgReader> mEpgReaderProvider =
-            new Provider<EpgReader>() {
-
-                @Override
-                public EpgReader get() {
-                    return new StubEpgReader(LiveTvApplication.this);
-                }
-            };
-
     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 ExperimentLoader mExperimentLoader;
-    private PerformanceMonitor mPerformanceMonitor;
+    @Inject PerformanceMonitor mPerformanceMonitor;
 
     @Override
     protected AndroidInjector<LiveTvApplication> applicationInjector() {
@@ -78,38 +62,15 @@
     @Override
     public void onCreate() {
         super.onCreate();
-        PERFORMANCE_MONITOR_MANAGER.getStartupMeasure().onAppCreate(this);
-    }
-
-    /** Returns the {@link AccountHelperImpl}. */
-    @Override
-    public AccountHelper getAccountHelper() {
-        if (mAccountHelper == null) {
-            mAccountHelper = new AccountHelperImpl(getApplicationContext());
-        }
-        return mAccountHelper;
+        STARTUP_MEASURE.onAppCreate(this);
     }
 
     @Override
-    public synchronized PerformanceMonitor getPerformanceMonitor() {
-        if (mPerformanceMonitor == null) {
-            mPerformanceMonitor = PerformanceMonitorManagerFactory.create().initialize(this);
-        }
+    public PerformanceMonitor getPerformanceMonitor() {
         return mPerformanceMonitor;
     }
 
     @Override
-    public Provider<EpgReader> providesEpgReader() {
-        return mEpgReaderProvider;
-    }
-
-    @Override
-    public ExperimentLoader getExperimentLoader() {
-        mExperimentLoader = new ExperimentLoader();
-        return mExperimentLoader;
-    }
-
-    @Override
     public DefaultBackendKnobsFlags getBackendKnobs() {
         return mBackendKnobsFlags;
     }
@@ -148,16 +109,6 @@
     }
 
     @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
index 3d3f049..71ce1a8 100644
--- a/src/com/android/tv/app/LiveTvApplicationComponent.java
+++ b/src/com/android/tv/app/LiveTvApplicationComponent.java
@@ -15,6 +15,7 @@
  */
 package com.android.tv.app;
 
+import com.android.tv.search.LocalSearchProvider;
 import dagger.Component;
 import dagger.android.AndroidInjectionModule;
 import dagger.android.AndroidInjector;
@@ -22,5 +23,10 @@
 
 /** Dagger component for {@link LiveTvApplication}. */
 @Singleton
-@Component(modules = {AndroidInjectionModule.class, LiveTvModule.class})
+@Component(
+    modules = {
+        AndroidInjectionModule.class,
+        LiveTvModule.class,
+        LocalSearchProvider.Module.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
index a28749b..db631bc 100644
--- a/src/com/android/tv/app/LiveTvModule.java
+++ b/src/com/android/tv/app/LiveTvModule.java
@@ -16,18 +16,49 @@
 package com.android.tv.app;
 
 import com.android.tv.common.flags.impl.DefaultFlagsModule;
+import com.android.tv.data.epg.EpgReader;
+import com.android.tv.data.epg.StubEpgReader;
 import com.android.tv.modules.TvApplicationModule;
+import com.android.tv.perf.PerformanceMonitor;
+import com.android.tv.perf.stub.StubPerformanceMonitor;
 import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
+import com.android.tv.ui.sidepanel.DeveloperOptionFragment;
+import com.android.tv.util.account.AccountHelper;
+import com.android.tv.util.account.AccountHelperImpl;
 import com.google.common.base.Optional;
 import dagger.Module;
 import dagger.Provides;
+import javax.inject.Singleton;
 
 /** Dagger module for {@link LiveTvApplication}. */
 @Module(includes = {DefaultFlagsModule.class, TvApplicationModule.class})
 class LiveTvModule {
 
     @Provides
+    static AccountHelper providesAccountHelper(AccountHelperImpl impl) {
+        return impl;
+    }
+
+    @Provides
+    static Optional<DeveloperOptionFragment.AdditionalDeveloperItemsFactory>
+            providesAdditionalDeveloperItemsFactory() {
+        return Optional.absent();
+    }
+
+    @Provides
     Optional<BuiltInTunerManager> providesBuiltInTunerManager() {
         return Optional.absent();
     }
+
+    @Provides
+    @Singleton
+    PerformanceMonitor providesPerformanceMonitor() {
+        return new StubPerformanceMonitor();
+    }
+
+    @Provides
+    @Singleton
+    static EpgReader providesEpgReader(StubEpgReader impl) {
+        return impl;
+    }
 }
diff --git a/src/com/android/tv/data/BaseProgram.java b/src/com/android/tv/data/BaseProgram.java
deleted file mode 100644
index 9650fd1..0000000
--- a/src/com/android/tv/data/BaseProgram.java
+++ /dev/null
@@ -1,208 +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.data;
-
-import android.content.Context;
-import android.media.tv.TvContentRating;
-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
- * com.android.tv.dvr.data.RecordedProgram}.
- */
-public abstract class BaseProgram {
-    /**
-     * Comparator used to compare {@link BaseProgram} according to its season and episodes number.
-     * If a program's season or episode number is null, it will be consider "smaller" than programs
-     * with season or episode numbers.
-     */
-    public static final Comparator<BaseProgram> EPISODE_COMPARATOR = new EpisodeComparator(false);
-
-    /**
-     * Comparator used to compare {@link BaseProgram} according to its season and episodes number
-     * with season numbers in a reversed order. If a program's season or episode number is null, it
-     * will be consider "smaller" than programs with season or episode numbers.
-     */
-    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;
-
-        EpisodeComparator(boolean reversedSeason) {
-            mReversedSeason = reversedSeason;
-        }
-
-        @Override
-        public int compare(BaseProgram lhs, BaseProgram rhs) {
-            if (lhs == rhs) {
-                return 0;
-            }
-            int seasonNumberCompare = numberCompare(lhs.getSeasonNumber(), rhs.getSeasonNumber());
-            if (seasonNumberCompare != 0) {
-                return mReversedSeason ? -seasonNumberCompare : seasonNumberCompare;
-            } else {
-                return numberCompare(lhs.getEpisodeNumber(), rhs.getEpisodeNumber());
-            }
-        }
-    }
-
-    /** Compares two strings represent season numbers or episode numbers of programs. */
-    public static int numberCompare(String s1, String s2) {
-        if (Objects.equals(s1, s2)) {
-            return 0;
-        } else if (s1 == null) {
-            return -1;
-        } else if (s2 == null) {
-            return 1;
-        } else if (s1.equals(s2)) {
-            return 0;
-        }
-        try {
-            return Integer.compare(Integer.parseInt(s1), Integer.parseInt(s2));
-        } catch (NumberFormatException e) {
-            return s1.compareTo(s2);
-        }
-    }
-
-    /** Returns ID of the program. */
-    public abstract long getId();
-
-    /** Returns the title of the program. */
-    public abstract String getTitle();
-
-    /** Returns the episode title. */
-    public abstract String getEpisodeTitle();
-
-    /** Returns the displayed title of the program episode. */
-    @Nullable
-    public String getEpisodeDisplayTitle(Context context) {
-        String episodeNumber = getEpisodeNumber();
-        String episodeTitle = getEpisodeTitle();
-        if (!TextUtils.isEmpty(episodeNumber)) {
-            episodeTitle = episodeTitle == null ? "" : episodeTitle;
-            String seasonNumber = getSeasonNumber();
-            if (TextUtils.isEmpty(seasonNumber) || TextUtils.equals(seasonNumber, "0")) {
-                // Do not show "S0: ".
-                return context.getResources()
-                        .getString(
-                                R.string.display_episode_title_format_no_season_number,
-                                episodeNumber,
-                                episodeTitle);
-            } else {
-                return context.getResources()
-                        .getString(
-                                R.string.display_episode_title_format,
-                                seasonNumber,
-                                episodeNumber,
-                                episodeTitle);
-            }
-        }
-        return episodeTitle;
-    }
-
-    /**
-     * Returns the content description of the program episode, suitable for being spoken by an
-     * accessibility service.
-     */
-    public String getEpisodeContentDescription(Context context) {
-        String episodeNumber = getEpisodeNumber();
-        String episodeTitle = getEpisodeTitle();
-        if (!TextUtils.isEmpty(episodeNumber)) {
-            episodeTitle = episodeTitle == null ? "" : episodeTitle;
-            String seasonNumber = getSeasonNumber();
-            if (TextUtils.isEmpty(seasonNumber) || TextUtils.equals(seasonNumber, "0")) {
-                // Do not list season if it is empty or 0
-                return context.getResources()
-                        .getString(
-                                R.string.content_description_episode_format_no_season_number,
-                                episodeNumber,
-                                episodeTitle);
-            } else {
-                return context.getResources()
-                        .getString(
-                                R.string.content_description_episode_format,
-                                seasonNumber,
-                                episodeNumber,
-                                episodeTitle);
-            }
-        }
-        return episodeTitle;
-    }
-
-    /** Returns the description of the program. */
-    public abstract String getDescription();
-
-    /** Returns the long description of the program. */
-    public abstract String getLongDescription();
-
-    /** Returns the start time of the program in Milliseconds. */
-    public abstract long getStartTimeUtcMillis();
-
-    /** Returns the end time of the program in Milliseconds. */
-    public abstract long getEndTimeUtcMillis();
-
-    /** Returns the duration of the program in Milliseconds. */
-    public abstract long getDurationMillis();
-
-    /** Returns the series ID. */
-    @Nullable
-    public abstract String getSeriesId();
-
-    /** Returns the season number. */
-    public abstract String getSeasonNumber();
-
-    /** Returns the episode number. */
-    public abstract String getEpisodeNumber();
-
-    /** Returns URI of the program's poster. */
-    public abstract String getPosterArtUri();
-
-    /** Returns URI of the program's thumbnail. */
-    public abstract String getThumbnailUri();
-
-    /** Returns the array of the ID's of the canonical genres. */
-    public abstract int[] getCanonicalGenreIds();
-
-    /** Returns the array of content ratings. */
-    public abstract ImmutableList<TvContentRating> getContentRatings();
-
-    /** Returns channel's ID of the program. */
-    public abstract long getChannelId();
-
-    /** Returns if the program is valid. */
-    public abstract boolean isValid();
-
-    /** Checks whether the program is episodic or not. */
-    public boolean isEpisodic() {
-        return getSeriesId() != null;
-    }
-
-    /** Generates the series ID for the other inputs than the tuner TV input. */
-    public static String generateSeriesId(String packageName, String title) {
-        return packageName + "/" + title;
-    }
-}
diff --git a/src/com/android/tv/data/BaseProgramImpl.java b/src/com/android/tv/data/BaseProgramImpl.java
new file mode 100644
index 0000000..9c8d025
--- /dev/null
+++ b/src/com/android/tv/data/BaseProgramImpl.java
@@ -0,0 +1,86 @@
+/*
+ * 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.data;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+
+import com.android.tv.R;
+import com.android.tv.data.api.BaseProgram;
+
+/** Base class for {@link ProgramImpl} and {@link com.android.tv.dvr.data.RecordedProgram}. */
+public abstract class BaseProgramImpl implements BaseProgram {
+
+    @Override
+    @Nullable
+    public String getEpisodeDisplayTitle(Context context) {
+        String episodeNumber = getEpisodeNumber();
+        String episodeTitle = getEpisodeTitle();
+        if (!TextUtils.isEmpty(episodeNumber)) {
+            episodeTitle = episodeTitle == null ? "" : episodeTitle;
+            String seasonNumber = getSeasonNumber();
+            if (TextUtils.isEmpty(seasonNumber) || TextUtils.equals(seasonNumber, "0")) {
+                // Do not show "S0: ".
+                return context.getResources()
+                        .getString(
+                                R.string.display_episode_title_format_no_season_number,
+                                episodeNumber,
+                                episodeTitle);
+            } else {
+                return context.getResources()
+                        .getString(
+                                R.string.display_episode_title_format,
+                                seasonNumber,
+                                episodeNumber,
+                                episodeTitle);
+            }
+        }
+        return episodeTitle;
+    }
+
+    @Override
+    public String getEpisodeContentDescription(Context context) {
+        String episodeNumber = getEpisodeNumber();
+        String episodeTitle = getEpisodeTitle();
+        if (!TextUtils.isEmpty(episodeNumber)) {
+            episodeTitle = episodeTitle == null ? "" : episodeTitle;
+            String seasonNumber = getSeasonNumber();
+            if (TextUtils.isEmpty(seasonNumber) || TextUtils.equals(seasonNumber, "0")) {
+                // Do not list season if it is empty or 0
+                return context.getResources()
+                        .getString(
+                                R.string.content_description_episode_format_no_season_number,
+                                episodeNumber,
+                                episodeTitle);
+            } else {
+                return context.getResources()
+                        .getString(
+                                R.string.content_description_episode_format,
+                                seasonNumber,
+                                episodeNumber,
+                                episodeTitle);
+            }
+        }
+        return episodeTitle;
+    }
+
+    @Override
+    public boolean isEpisodic() {
+        return !TextUtils.isEmpty(getSeriesId());
+    }
+}
diff --git a/src/com/android/tv/data/ChannelDataManager.java b/src/com/android/tv/data/ChannelDataManager.java
index a5c786c..67c3230 100644
--- a/src/com/android/tv/data/ChannelDataManager.java
+++ b/src/com/android/tv/data/ChannelDataManager.java
@@ -37,15 +37,18 @@
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.MutableInt;
-import com.android.tv.TvSingletons;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.WeakHandler;
+import com.android.tv.common.dagger.annotations.ApplicationContext;
 import com.android.tv.common.util.PermissionUtils;
 import com.android.tv.common.util.SharedPreferencesUtils;
 import com.android.tv.data.api.Channel;
 import com.android.tv.util.AsyncDbTask;
+import com.android.tv.util.AsyncDbTask.DbExecutor;
 import com.android.tv.util.TvInputManagerHelper;
 import com.android.tv.util.Utils;
+import com.google.auto.factory.AutoFactory;
+import com.google.auto.factory.Provided;
 import java.io.FileNotFoundException;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -56,6 +59,7 @@
 import java.util.Set;
 import java.util.concurrent.CopyOnWriteArraySet;
 import java.util.concurrent.Executor;
+import javax.inject.Singleton;
 
 /**
  * The class to manage channel data. Basic features: reading channel list and each channel's current
@@ -64,6 +68,8 @@
  * methods are called in only the main thread.
  */
 @AnyThread
+@AutoFactory
+@Singleton
 public class ChannelDataManager {
     private static final String TAG = "ChannelDataManager";
     private static final boolean DEBUG = false;
@@ -143,21 +149,11 @@
             };
 
     @MainThread
-    public ChannelDataManager(Context context, TvInputManagerHelper inputManager) {
-        this(
-                context,
-                inputManager,
-                TvSingletons.getSingletons(context).getDbExecutor(),
-                context.getContentResolver());
-    }
-
-    @MainThread
-    @VisibleForTesting
-    ChannelDataManager(
-            Context context,
-            TvInputManagerHelper inputManager,
-            Executor executor,
-            ContentResolver contentResolver) {
+    public ChannelDataManager(
+            @Provided @ApplicationContext Context context,
+            @Provided TvInputManagerHelper inputManager,
+            @Provided @DbExecutor Executor executor,
+            @Provided ContentResolver contentResolver) {
         mContext = context;
         mInputManager = inputManager;
         mDbExecutor = executor;
@@ -729,7 +725,7 @@
     /**
      * Updates a column {@code columnName} of DB table {@code uri} with the value {@code
      * columnValue}. The selective rows in the ID list {@code ids} will be updated. The DB
-     * operations will run on {@link TvSingletons#getDbExecutor()}.
+     * operations will run on @{@link DbExecutor}.
      */
     private void updateOneColumnValue(
             final String columnName, final int columnValue, final List<Long> ids) {
diff --git a/src/com/android/tv/data/InternalDataUtils.java b/src/com/android/tv/data/InternalDataUtils.java
index 4c30d39..b17ed09 100644
--- a/src/com/android/tv/data/InternalDataUtils.java
+++ b/src/com/android/tv/data/InternalDataUtils.java
@@ -19,8 +19,11 @@
 import android.support.annotation.Nullable;
 import android.text.TextUtils;
 import android.util.Log;
-import com.android.tv.data.Program.CriticScore;
+
+import com.android.tv.data.api.Program;
+import com.android.tv.data.api.Program.CriticScore;
 import com.android.tv.dvr.data.RecordedProgram;
+
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -49,7 +52,7 @@
      * @param bytes the bytes to be deserialized
      * @param builder the builder for the Program class
      */
-    public static void deserializeInternalProviderData(byte[] bytes, Program.Builder builder) {
+    public static void deserializeInternalProviderData(byte[] bytes, ProgramImpl.Builder builder) {
         if (bytes == null || bytes.length == 0) {
             return;
         }
diff --git a/src/com/android/tv/data/OnCurrentProgramUpdatedListener.java b/src/com/android/tv/data/OnCurrentProgramUpdatedListener.java
index edb3355..2332cda 100644
--- a/src/com/android/tv/data/OnCurrentProgramUpdatedListener.java
+++ b/src/com/android/tv/data/OnCurrentProgramUpdatedListener.java
@@ -16,6 +16,8 @@
 
 package com.android.tv.data;
 
+import com.android.tv.data.api.Program;
+
 public interface OnCurrentProgramUpdatedListener {
     /** Called when the current program is updated. */
     void onCurrentProgramUpdated(long channelId, Program program);
diff --git a/src/com/android/tv/data/PreviewDataManager.java b/src/com/android/tv/data/PreviewDataManager.java
index 8616aee..dbe6028 100644
--- a/src/com/android/tv/data/PreviewDataManager.java
+++ b/src/com/android/tv/data/PreviewDataManager.java
@@ -32,10 +32,14 @@
 import android.support.annotation.MainThread;
 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 com.android.tv.util.images.ImageLoader;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.HashMap;
@@ -218,7 +222,7 @@
                 Uri previewChannelsUri =
                         PreviewDataUtils.addQueryParamToUri(
                                 TvContract.Channels.CONTENT_URI,
-                                new Pair<>(PARAM_PREVIEW, String.valueOf(true)));
+                                Pair.create(PARAM_PREVIEW, String.valueOf(true)));
                 String packageName = mContext.getPackageName();
                 if (PermissionUtils.hasAccessAllEpg(mContext)) {
                     try (Cursor cursor =
@@ -428,10 +432,14 @@
                     continue;
                 }
                 try {
+                    int aspectRatio =
+                            ImageLoader.getAspectRatioFromPosterArtUri(
+                                    mContext, program.getPosterArtUri().toString());
                     Uri programUri =
                             mContentResolver.insert(
                                     TvContract.PreviewPrograms.CONTENT_URI,
-                                    PreviewDataUtils.createPreviewProgramFromContent(program)
+                                    PreviewDataUtils.createPreviewProgramFromContent(
+                                                    program, aspectRatio)
                                             .toContentValues());
                     if (programUri != null) {
                         long previewProgramId = ContentUris.parseId(programUri);
@@ -592,13 +600,14 @@
 
         /** Creates a preview program. */
         public static PreviewProgram createPreviewProgramFromContent(
-                PreviewProgramContent program) {
+                PreviewProgramContent program, int aspectRatio) {
             PreviewProgram.Builder builder = new PreviewProgram.Builder();
             builder.setChannelId(program.getPreviewChannelId())
                     .setType(program.getType())
                     .setLive(program.getLive())
                     .setTitle(program.getTitle())
                     .setDescription(program.getDescription())
+                    .setPosterArtAspectRatio(aspectRatio)
                     .setPosterArtUri(program.getPosterArtUri())
                     .setIntentUri(program.getIntentUri())
                     .setPreviewVideoUri(program.getPreviewVideoUri())
diff --git a/src/com/android/tv/data/PreviewProgramContent.java b/src/com/android/tv/data/PreviewProgramContent.java
index 8d4b88c..4ee5710 100644
--- a/src/com/android/tv/data/PreviewProgramContent.java
+++ b/src/com/android/tv/data/PreviewProgramContent.java
@@ -21,10 +21,14 @@
 import android.support.annotation.VisibleForTesting;
 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.data.api.Program;
 import com.android.tv.dvr.data.RecordedProgram;
+
 import java.util.Objects;
 
 /** A class to store the content of preview programs. */
@@ -41,7 +45,7 @@
     private Uri mIntentUri;
     private Uri mPreviewVideoUri;
 
-    /** Create preview program content from {@link Program} */
+    /** Create preview program content from {@link ProgramImpl} */
     public static PreviewProgramContent createFromProgram(
             Context context, long previewChannelId, Program program) {
         Channel channel =
@@ -79,7 +83,7 @@
                 .setIntentUri(channel.getUri())
                 .setPreviewVideoUri(
                         PreviewDataManager.PreviewDataUtils.addQueryParamToUri(
-                                channel.getUri(), new Pair<>(PARAM_INPUT, channel.getInputId())))
+                                channel.getUri(), Pair.create(PARAM_INPUT, channel.getInputId())))
                 .build();
     }
 
@@ -99,7 +103,7 @@
                 .setPreviewVideoUri(
                         PreviewDataManager.PreviewDataUtils.addQueryParamToUri(
                                 recordedProgramUri,
-                                new Pair<>(PARAM_INPUT, recordedProgram.getInputId())))
+                                Pair.create(PARAM_INPUT, recordedProgram.getInputId())))
                 .build();
     }
 
diff --git a/src/com/android/tv/data/ProgramDataManager.java b/src/com/android/tv/data/ProgramDataManager.java
index 2f20c89..a866c78 100644
--- a/src/com/android/tv/data/ProgramDataManager.java
+++ b/src/com/android/tv/data/ProgramDataManager.java
@@ -33,19 +33,24 @@
 import android.util.Log;
 import android.util.LongSparseArray;
 import android.util.LruCache;
+
 import com.android.tv.TvSingletons;
 import com.android.tv.common.SoftPreconditions;
 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.data.api.Program;
 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.TvInputManagerHelper;
 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;
@@ -75,6 +80,11 @@
     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);
 
+    // Default fetch hours
+    private static final long FETCH_HOURS_MS = TimeUnit.HOURS.toMillis(24);
+    // Load data earlier for smooth scrolling.
+    private static final long BUFFER_HOURS_MS = TimeUnit.HOURS.toMillis(6);
+
     // TODO: Use TvContract constants, once they become public.
     private static final String PARAM_START_TIME = "start_time";
     private static final String PARAM_END_TIME = "end_time";
@@ -86,10 +96,20 @@
                     + Programs.COLUMN_CHANNEL_ID
                     + ", "
                     + Programs.COLUMN_END_TIME_UTC_MILLIS;
+    private static final String SORT_BY_CHANNEL_ID =
+            Programs.COLUMN_CHANNEL_ID
+                    + ", "
+                    + Programs.COLUMN_START_TIME_UTC_MILLIS
+                    + " DESC, "
+                    + Programs.COLUMN_END_TIME_UTC_MILLIS
+                    + " ASC, "
+                    + Programs._ID
+                    + " DESC";
 
     private static final int MSG_UPDATE_CURRENT_PROGRAMS = 1000;
     private static final int MSG_UPDATE_ONE_CURRENT_PROGRAM = 1001;
     private static final int MSG_UPDATE_PREFETCH_PROGRAM = 1002;
+    private static final int MSG_UPDATE_CONTENT_RATINGS = 1003;
 
     private final Context mContext;
     private final Clock mClock;
@@ -98,6 +118,7 @@
     private final BackendKnobsFlags mBackendKnobsFlags;
     private final PerformanceMonitor mPerformanceMonitor;
     private final ChannelDataManager mChannelDataManager;
+    private final TvInputManagerHelper mTvInputManagerHelper;
     private boolean mStarted;
     // Updated only on the main thread.
     private volatile boolean mCurrentProgramsLoadFinished;
@@ -125,6 +146,11 @@
 
     private boolean mPauseProgramUpdate = false;
     private final LruCache<Long, Program> mZeroLengthProgramCache = new LruCache<>(10);
+    // Current tuned channel.
+    private long mTunedChannelId;
+    // Hours of data to be fetched, it is updated during horizontal scroll.
+    // Note that it should never exceed programGuideMaxHours.
+    private long mMaxFetchHoursMs = FETCH_HOURS_MS;
 
     @MainThread
     public ProgramDataManager(Context context) {
@@ -136,7 +162,8 @@
                 Looper.myLooper(),
                 TvSingletons.getSingletons(context).getBackendKnobs(),
                 TvSingletons.getSingletons(context).getPerformanceMonitor(),
-                TvSingletons.getSingletons(context).getChannelDataManager());
+                TvSingletons.getSingletons(context).getChannelDataManager(),
+                TvSingletons.getSingletons(context).getTvInputManagerHelper());
     }
 
     @VisibleForTesting
@@ -148,7 +175,8 @@
             Looper looper,
             BackendKnobsFlags backendKnobsFlags,
             PerformanceMonitor performanceMonitor,
-            ChannelDataManager channelDataManager) {
+            ChannelDataManager channelDataManager,
+            TvInputManagerHelper tvInputManagerHelper) {
         mContext = context;
         mDbExecutor = executor;
         mClock = time;
@@ -157,6 +185,7 @@
         mBackendKnobsFlags = backendKnobsFlags;
         mPerformanceMonitor = performanceMonitor;
         mChannelDataManager = channelDataManager;
+        mTvInputManagerHelper = tvInputManagerHelper;
         mProgramObserver =
                 new ContentObserver(mHandler) {
                     @Override
@@ -205,6 +234,7 @@
         // Should be called directly instead of posting MSG_UPDATE_CURRENT_PROGRAMS message
         // to the handler. If not, another DB task can be executed before loading current programs.
         handleUpdateCurrentPrograms();
+        mHandler.sendEmptyMessage(MSG_UPDATE_CONTENT_RATINGS);
         if (mPrefetchEnabled) {
             mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
         }
@@ -259,17 +289,69 @@
         }
     }
 
-    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());
+    /**
+     * Prefetch program data if needed.
+     *
+     * @param channelId ID of the channel to prefetch
+     * @param selectedProgramIndex index of selected program.
+     */
+    public void prefetchChannel(long channelId, int selectedProgramIndex) {
+        long startTimeMs =
+                Utils.floorTime(
+                        mClock.currentTimeMillis() - PROGRAM_GUIDE_SNAP_TIME_MS,
+                        PROGRAM_GUIDE_SNAP_TIME_MS);
+        long programGuideMaxHoursMs =
+                TimeUnit.HOURS.toMillis(mBackendKnobsFlags.programGuideMaxHours());
+        long endTimeMs = 0;
+        if (mMaxFetchHoursMs < programGuideMaxHoursMs
+                && isHorizontalLoadNeeded(startTimeMs, channelId, selectedProgramIndex)) {
+            // Horizontal scrolling needs to load data of further days.
+            mMaxFetchHoursMs = Math.min(programGuideMaxHoursMs, mMaxFetchHoursMs + FETCH_HOURS_MS);
+            mCompleteInfoChannelIds.clear();
+        }
+        // Load max hours complete data for first channel.
+        if (mCompleteInfoChannelIds.isEmpty()) {
+            endTimeMs = startTimeMs + programGuideMaxHoursMs;
+        } else if (!mCompleteInfoChannelIds.contains(channelId)) {
+            endTimeMs = startTimeMs + mMaxFetchHoursMs;
+        }
+        if (endTimeMs > 0) {
+            mCompleteInfoChannelIds.add(channelId);
             new SingleChannelPrefetchTask(channelId, startTimeMs, endTimeMs).executeOnDbThread();
         }
     }
 
+    public void prefetchChannel(long channelId) {
+        prefetchChannel(channelId, 0);
+    }
+
+    /**
+     * Check if enough data is present for horizontal scroll, otherwise prefetch programs.
+     *
+     * <p>If end time of current program is past {@code BUFFER_HOURS_MS} less than the fetched time
+     * we need to prefetch proceeding programs.
+     *
+     * @param startTimeMs Fetch start time, it is used to get fetch end time.
+     * @param channelId
+     * @param selectedProgramIndex
+     * @return {@code true} If data load is needed, else {@code false}.
+     */
+    private boolean isHorizontalLoadNeeded(
+            long startTimeMs, long channelId, int selectedProgramIndex) {
+        if (mChannelIdProgramCache.containsKey(channelId)) {
+            ArrayList<Program> programs = mChannelIdProgramCache.get(channelId);
+            long marginEndTime = startTimeMs + mMaxFetchHoursMs - BUFFER_HOURS_MS;
+            return programs.size() > selectedProgramIndex &&
+                    programs.get(selectedProgramIndex).getEndTimeUtcMillis() > marginEndTime;
+        }
+        return false;
+    }
+
+    public void onChannelTuned(long channelId) {
+        mTunedChannelId = channelId;
+        prefetchChannel(channelId);
+    }
+
     /** A Callback interface to receive notification on program data retrieval from DB. */
     public interface Callback {
         /**
@@ -280,12 +362,10 @@
         void onProgramUpdated();
 
         /**
-         * Called when we update complete program data of specific channel during scrolling. Data is
-         * loaded from DB on request basis.
-         *
-         * @param channelId
+         * Called when we update program data during scrolling. Data is loaded from DB on request
+         * basis. It loads data based on horizontal scrolling as well.
          */
-        void onSingleChannelUpdated(long channelId);
+        void onChannelUpdated();
     }
 
     /** Adds the {@link Callback}. */
@@ -312,7 +392,7 @@
         } else {
             mPrefetchEnabled = false;
             cancelPrefetchTask();
-            mChannelIdProgramCache.clear();
+            clearChannelInfoMap();
             mHandler.removeMessages(MSG_UPDATE_PREFETCH_PROGRAM);
         }
     }
@@ -539,10 +619,7 @@
                 }
                 programMap.clear();
 
-                String[] projection =
-                        mBackendKnobsFlags.enablePartialProgramFetch()
-                                ? Program.PARTIAL_PROJECTION
-                                : Program.PROJECTION;
+                String[] projection = ProgramImpl.PARTIAL_PROJECTION;
                 if (TvProviderUtils.checkSeriesIdColumn(mContext, Programs.CONTENT_URI)) {
                     if (Utils.isProgramsUri(uri)) {
                         projection =
@@ -562,10 +639,7 @@
                             }
                             return null;
                         }
-                        Program program =
-                                mBackendKnobsFlags.enablePartialProgramFetch()
-                                        ? Program.fromCursorPartialProjection(c)
-                                        : Program.fromCursor(c);
+                        Program program = ProgramImpl.fromCursorPartialProjection(c);
                         if (Program.isDuplicate(program, lastReadProgram)) {
                             duplicateCount++;
                             continue;
@@ -575,15 +649,14 @@
                         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;
-                                }
+                            // 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);
@@ -628,15 +701,12 @@
                                         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();
-                }
+                // Since cache has partial data we need to reset the map of complete data.
+                clearChannelInfoMap();
+                // Get complete projection of tuned channel.
+                prefetchChannel(mTunedChannelId);
+
                 notifyProgramUpdated();
                 if (mFromEmptyCacheTimeEvent != null) {
                     mPerformanceMonitor.stopTimer(
@@ -654,6 +724,11 @@
         }
     }
 
+    private void clearChannelInfoMap() {
+        mCompleteInfoChannelIds.clear();
+        mMaxFetchHoursMs = FETCH_HOURS_MS;
+    }
+
     private long getFetchDuration() {
         if (mChannelIdProgramCache.isEmpty()) {
             return Math.max(1L, mBackendKnobsFlags.programGuideInitialFetchHours());
@@ -685,7 +760,7 @@
                     mDbExecutor,
                     mContext,
                     TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs),
-                    Program.PROJECTION,
+                    ProgramImpl.PROJECTION,
                     null,
                     null,
                     SORT_BY_TIME);
@@ -696,7 +771,7 @@
         protected ArrayList<Program> onQuery(Cursor c) {
             ArrayList<Program> programMap = new ArrayList<>();
             while (c.moveToNext()) {
-                Program program = Program.fromCursor(c);
+                Program program = ProgramImpl.fromCursor(c);
                 programMap.add(program);
             }
             return programMap;
@@ -705,7 +780,7 @@
         @Override
         protected void onPostExecute(ArrayList<Program> programs) {
             mChannelIdProgramCache.put(mChannelId, programs);
-            notifySingleChannelUpdated(mChannelId);
+            notifyChannelUpdated();
         }
     }
 
@@ -715,9 +790,9 @@
         }
     }
 
-    private void notifySingleChannelUpdated(long channelId) {
+    private void notifyChannelUpdated() {
         for (Callback callback : mCallbacks) {
-            callback.onSingleChannelUpdated(channelId);
+            callback.onChannelUpdated();
         }
     }
 
@@ -731,10 +806,10 @@
                             .appendQueryParameter(PARAM_START_TIME, String.valueOf(time))
                             .appendQueryParameter(PARAM_END_TIME, String.valueOf(time))
                             .build(),
-                    Program.PROJECTION,
+                    ProgramImpl.PROJECTION,
                     null,
                     null,
-                    SORT_BY_TIME);
+                    SORT_BY_CHANNEL_ID);
         }
 
         @Override
@@ -747,17 +822,21 @@
                     if (isCancelled()) {
                         return programs;
                     }
-                    Program program = Program.fromCursor(c);
-                    if (Program.isDuplicate(program, lastReadProgram)) {
+                    Program program = ProgramImpl.fromCursor(c);
+                    // Only one program is expected per channel for this query
+                    // However, skip overlapping programs from same channel
+                    if (Program.sameChannel(program, lastReadProgram)
+                            && Program.isOverlapping(program, lastReadProgram)) {
                         duplicateCount++;
                         continue;
                     } else {
                         lastReadProgram = program;
                     }
+
                     programs.add(program);
                 }
                 if (duplicateCount > 0) {
-                    Log.w(TAG, "Found " + duplicateCount + " duplicate programs");
+                    Log.w(TAG, "Found " + duplicateCount + " overlapping programs");
                 }
             }
             return programs;
@@ -777,9 +856,7 @@
                 for (Long channelId : removedChannelIds) {
                     if (mPrefetchEnabled) {
                         mChannelIdProgramCache.remove(channelId);
-                        if (mBackendKnobsFlags.enablePartialProgramFetch()) {
-                            mCompleteInfoChannelIds.remove(channelId);
-                        }
+                        mCompleteInfoChannelIds.remove(channelId);
                     }
                     mChannelIdCurrentProgramMap.remove(channelId);
                     notifyCurrentProgramUpdate(channelId, null);
@@ -797,7 +874,7 @@
                     mDbExecutor,
                     mContext,
                     TvContract.buildProgramsUriForChannel(channelId, time, time),
-                    Program.PROJECTION,
+                    ProgramImpl.PROJECTION,
                     null,
                     null,
                     SORT_BY_TIME);
@@ -808,7 +885,7 @@
         public Program onQuery(Cursor c) {
             Program program = null;
             if (c != null && c.moveToNext()) {
-                program = Program.fromCursor(c);
+                program = ProgramImpl.fromCursor(c);
             }
             return program;
         }
@@ -869,6 +946,9 @@
                         }
                         break;
                     }
+                case MSG_UPDATE_CONTENT_RATINGS:
+                    mTvInputManagerHelper.getContentRatingsManager().update();
+                    break;
                 default:
                     // Do nothing
             }
@@ -932,7 +1012,7 @@
 
     // Create dummy program which indicates data isn't loaded yet so DB query is required.
     private Program createDummyProgram(long startTimeMs, long endTimeMs) {
-        return new Program.Builder()
+        return new ProgramImpl.Builder()
                 .setChannelId(Channel.INVALID_ID)
                 .setStartTimeUtcMillis(startTimeMs)
                 .setEndTimeUtcMillis(endTimeMs)
diff --git a/src/com/android/tv/data/Program.java b/src/com/android/tv/data/ProgramImpl.java
similarity index 86%
rename from src/com/android/tv/data/Program.java
rename to src/com/android/tv/data/ProgramImpl.java
index b688927..5097e2d 100644
--- a/src/com/android/tv/data/Program.java
+++ b/src/com/android/tv/data/ProgramImpl.java
@@ -32,24 +32,26 @@
 import android.support.annotation.VisibleForTesting;
 import android.support.annotation.WorkerThread;
 import android.text.TextUtils;
-import android.util.Log;
-import com.android.tv.common.BuildConfig;
+
 import com.android.tv.common.TvContentRatingCache;
 import com.android.tv.common.util.CollectionUtils;
 import com.android.tv.common.util.CommonUtils;
+import com.android.tv.data.api.BaseProgram;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 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;
 import java.util.List;
 import java.util.Objects;
 
 /** A convenience class to create and insert program information entries into the database. */
-public final class Program extends BaseProgram implements Comparable<Program>, Parcelable {
+public final class ProgramImpl extends BaseProgramImpl implements Parcelable, Program {
     private static final boolean DEBUG = false;
     private static final boolean DEBUG_DUMP_DESCRIPTION = false;
     private static final String TAG = "Program";
@@ -179,8 +181,8 @@
         return builder.build();
     }
 
-    public static Program fromParcel(Parcel in) {
-        Program program = new Program();
+    public static ProgramImpl fromParcel(Parcel in) {
+        ProgramImpl program = new ProgramImpl();
         program.mId = in.readLong();
         program.mPackageName = in.readString();
         program.mChannelId = in.readLong();
@@ -219,7 +221,7 @@
             new Parcelable.Creator<Program>() {
                 @Override
                 public Program createFromParcel(Parcel in) {
-                    return Program.fromParcel(in);
+                    return ProgramImpl.fromParcel(in);
                 }
 
                 @Override
@@ -251,19 +253,21 @@
     private ImmutableList<TvContentRating> mContentRatings;
     private boolean mRecordingProhibited;
 
-    private Program() {
+    private ProgramImpl() {
         // Do nothing.
     }
 
+    @Override
     public long getId() {
         return mId;
     }
 
-    /** Returns the package name of this program. */
+    @Override
     public String getPackageName() {
         return mPackageName;
     }
 
+    @Override
     public long getChannelId() {
         return mChannelId;
     }
@@ -274,11 +278,6 @@
         return mChannelId >= 0;
     }
 
-    /** Returns {@code true} if the program is valid and {@code false} otherwise. */
-    public static boolean isProgramValid(Program program) {
-        return program != null && program.isValid();
-    }
-
     @Override
     public String getTitle() {
         return mTitle;
@@ -302,6 +301,11 @@
     }
 
     @Override
+    public String getSeasonTitle() {
+        return mSeasonTitle;
+    }
+
+    @Override
     public String getEpisodeNumber() {
         return mEpisodeNumber;
     }
@@ -316,6 +320,7 @@
         return mEndTimeUtcMillis;
     }
 
+    @Override
     public String getDurationString(Context context) {
         // TODO(b/71717446): expire the calculated string
         if (mDurationString == null) {
@@ -341,15 +346,17 @@
         return mLongDescription;
     }
 
+    @Override
     public int getVideoWidth() {
         return mVideoWidth;
     }
 
+    @Override
     public int getVideoHeight() {
         return mVideoHeight;
     }
 
-    /** Returns the list of Critic Scores for this program */
+    @Override
     @Nullable
     public List<CriticScore> getCriticScores() {
         return mCriticScores;
@@ -371,12 +378,12 @@
         return mThumbnailUri;
     }
 
-    /** Returns {@code true} if the recording of this program is prohibited. */
+    @Override
     public boolean isRecordingProhibited() {
         return mRecordingProhibited;
     }
 
-    /** Returns array of canonical genres for this program. This is expected to be called rarely. */
+    @Override
     @Nullable
     public String[] getCanonicalGenres() {
         if (mCanonicalGenreIds == null) {
@@ -395,7 +402,7 @@
         return mCanonicalGenreIds;
     }
 
-    /** Returns if this program has the genre. */
+    @Override
     public boolean hasGenre(int genreId) {
         if (genreId == GenreItems.ID_ALL_CHANNELS) {
             return true;
@@ -436,11 +443,11 @@
 
     @Override
     public boolean equals(Object other) {
-        if (!(other instanceof Program)) {
+        if (!(other instanceof ProgramImpl)) {
             return false;
         }
         // Compare all the properties because program ID can be invalid for the dummy programs.
-        Program program = (Program) other;
+        ProgramImpl program = (ProgramImpl) other;
         return Objects.equals(mPackageName, program.mPackageName)
                 && mChannelId == program.mChannelId
                 && mStartTimeUtcMillis == program.mStartTimeUtcMillis
@@ -464,7 +471,7 @@
 
     @Override
     public int compareTo(@NonNull Program other) {
-        return Long.compare(mStartTimeUtcMillis, other.mStartTimeUtcMillis);
+        return Long.compare(mStartTimeUtcMillis, other.getStartTimeUtcMillis());
     }
 
     @Override
@@ -516,7 +523,7 @@
     }
 
     /**
-     * Translates a {@link Program} to {@link ContentValues} that are ready to be written into
+     * Translates a {@link ProgramImpl} to {@link ContentValues} that are ready to be written into
      * Database.
      */
     @SuppressLint("InlinedApi")
@@ -595,37 +602,37 @@
             return;
         }
 
-        mId = other.mId;
-        mPackageName = other.mPackageName;
-        mChannelId = other.mChannelId;
-        mTitle = other.mTitle;
-        mSeriesId = other.mSeriesId;
-        mEpisodeTitle = other.mEpisodeTitle;
-        mSeasonNumber = other.mSeasonNumber;
-        mSeasonTitle = other.mSeasonTitle;
-        mEpisodeNumber = other.mEpisodeNumber;
-        mStartTimeUtcMillis = other.mStartTimeUtcMillis;
-        mEndTimeUtcMillis = other.mEndTimeUtcMillis;
+        mId = other.getId();
+        mPackageName = other.getPackageName();
+        mChannelId = other.getChannelId();
+        mTitle = other.getTitle();
+        mSeriesId = other.getSeriesId();
+        mEpisodeTitle = other.getEpisodeTitle();
+        mSeasonNumber = other.getSeasonNumber();
+        mSeasonTitle = other.getSeasonTitle();
+        mEpisodeNumber = other.getEpisodeNumber();
+        mStartTimeUtcMillis = other.getStartTimeUtcMillis();
+        mEndTimeUtcMillis = other.getEndTimeUtcMillis();
         mDurationString = null; // Recreate Duration when needed.
-        mDescription = other.mDescription;
-        mLongDescription = other.mLongDescription;
-        mVideoWidth = other.mVideoWidth;
-        mVideoHeight = other.mVideoHeight;
-        mCriticScores = other.mCriticScores;
-        mPosterArtUri = other.mPosterArtUri;
-        mThumbnailUri = other.mThumbnailUri;
-        mCanonicalGenreIds = other.mCanonicalGenreIds;
-        mContentRatings = other.mContentRatings;
-        mRecordingProhibited = other.mRecordingProhibited;
+        mDescription = other.getDescription();
+        mLongDescription = other.getLongDescription();
+        mVideoWidth = other.getVideoWidth();
+        mVideoHeight = other.getVideoHeight();
+        mCriticScores = other.getCriticScores();
+        mPosterArtUri = other.getPosterArtUri();
+        mThumbnailUri = other.getThumbnailUri();
+        mCanonicalGenreIds = other.getCanonicalGenreIds();
+        mContentRatings = other.getContentRatings();
+        mRecordingProhibited = other.isRecordingProhibited();
     }
 
     /** A Builder for the Program class */
     public static final class Builder {
-        private final Program mProgram;
+        private final ProgramImpl mProgram;
 
         /** Creates a Builder for this Program class */
         public Builder() {
-            mProgram = new Program();
+            mProgram = new ProgramImpl();
             // Fill initial data.
             mProgram.mPackageName = null;
             mProgram.mChannelId = Channel.INVALID_ID;
@@ -650,7 +657,7 @@
          */
         @VisibleForTesting
         public Builder(Program other) {
-            mProgram = new Program();
+            mProgram = new ProgramImpl();
             mProgram.copyFrom(other);
         }
 
@@ -906,7 +913,7 @@
          *
          * @return the Program object constructed
          */
-        public Program build() {
+        public ProgramImpl build() {
             // Generate the series ID for the episodic program of other TV input.
             if (TextUtils.isEmpty(mProgram.mTitle)) {
                 // If title is null, series cannot be generated for this program.
@@ -916,17 +923,13 @@
                 // If series ID is not set, generate it for the episodic program of other TV input.
                 setSeriesId(BaseProgram.generateSeriesId(mProgram.mPackageName, mProgram.mTitle));
             }
-            Program program = new Program();
+            ProgramImpl program = new ProgramImpl();
             program.copyFrom(mProgram);
             return program;
         }
     }
 
-    /**
-     * Prefetches the program poster art.
-     *
-     * <p>
-     */
+    @Override
     public void prefetchPosterArt(Context context, int posterArtWidth, int posterArtHeight) {
         if (mPosterArtUri == null) {
             return;
@@ -934,20 +937,13 @@
         ImageLoader.prefetchBitmap(context, mPosterArtUri, posterArtWidth, posterArtHeight);
     }
 
-    /**
-     * Loads the program poster art and returns it via {@code callback}.
-     *
-     * <p>Note that it may directly call {@code callback} if the program poster art already is
-     * loaded.
-     *
-     * @return {@code true} if the load is complete and the callback is executed.
-     */
+    @Override
     @UiThread
     public boolean loadPosterArt(
             Context context,
             int posterArtWidth,
             int posterArtHeight,
-            ImageLoader.ImageLoaderCallback callback) {
+            ImageLoader.ImageLoaderCallback<?> callback) {
         if (mPosterArtUri == null) {
             return false;
         }
@@ -955,24 +951,9 @@
                 context, mPosterArtUri, posterArtWidth, posterArtHeight, callback);
     }
 
-    public static boolean isDuplicate(Program p1, Program p2) {
-        if (p1 == null || p2 == null) {
-            return false;
-        }
-        boolean isDuplicate =
-                p1.getChannelId() == p2.getChannelId()
-                        && p1.getStartTimeUtcMillis() == p2.getStartTimeUtcMillis()
-                        && p1.getEndTimeUtcMillis() == p2.getEndTimeUtcMillis();
-        if (DEBUG && BuildConfig.ENG && isDuplicate) {
-            Log.w(
-                    TAG,
-                    "Duplicate programs detected! - \""
-                            + p1.getTitle()
-                            + "\" and \""
-                            + p2.getTitle()
-                            + "\"");
-        }
-        return isDuplicate;
+    @Override
+    public Parcelable toParcelable() {
+        return this;
     }
 
     @Override
@@ -1009,54 +990,4 @@
         }
         out.writeByte((byte) (mRecordingProhibited ? 1 : 0));
     }
-
-    /** Holds one type of critic score and its source. */
-    public static final class CriticScore implements Serializable, Parcelable {
-        /** The source of the rating. */
-        public final String source;
-        /** The score. */
-        public final String score;
-        /** The url of the logo image */
-        public final String logoUrl;
-
-        public static final Parcelable.Creator<CriticScore> CREATOR =
-                new Parcelable.Creator<CriticScore>() {
-                    @Override
-                    public CriticScore createFromParcel(Parcel in) {
-                        String source = in.readString();
-                        String score = in.readString();
-                        String logoUri = in.readString();
-                        return new CriticScore(source, score, logoUri);
-                    }
-
-                    @Override
-                    public CriticScore[] newArray(int size) {
-                        return new CriticScore[size];
-                    }
-                };
-
-        /**
-         * Constructor for this class.
-         *
-         * @param source the source of the rating
-         * @param score the score
-         */
-        public CriticScore(String source, String score, String logoUrl) {
-            this.source = source;
-            this.score = score;
-            this.logoUrl = logoUrl;
-        }
-
-        @Override
-        public int describeContents() {
-            return 0;
-        }
-
-        @Override
-        public void writeToParcel(Parcel out, int i) {
-            out.writeString(source);
-            out.writeString(score);
-            out.writeString(logoUrl);
-        }
-    }
 }
diff --git a/src/com/android/tv/data/api/BaseProgram.java b/src/com/android/tv/data/api/BaseProgram.java
new file mode 100644
index 0000000..8acaf79
--- /dev/null
+++ b/src/com/android/tv/data/api/BaseProgram.java
@@ -0,0 +1,141 @@
+package com.android.tv.data.api;
+
+import android.content.Context;
+import android.media.tv.TvContentRating;
+import android.support.annotation.Nullable;
+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
+ * com.android.tv.dvr.data.RecordedProgram}.
+ */
+public interface BaseProgram {
+
+    /**
+     * Comparator used to compare {@link BaseProgram} according to its season and episodes number.
+     * If a program's season or episode number is null, it will be consider "smaller" than programs
+     * with season or episode numbers.
+     */
+    Comparator<BaseProgram> EPISODE_COMPARATOR = new EpisodeComparator(false);
+    /**
+     * Comparator used to compare {@link BaseProgram} according to its season and episodes number
+     * with season numbers in a reversed order. If a program's season or episode number is null, it
+     * will be consider "smaller" than programs with season or episode numbers.
+     */
+    Comparator<BaseProgram> SEASON_REVERSED_EPISODE_COMPARATOR = new EpisodeComparator(true);
+
+    String COLUMN_SERIES_ID = "series_id";
+    String COLUMN_STATE = "state";
+
+    /** Compares two strings represent season numbers or episode numbers of programs. */
+    static int numberCompare(String s1, String s2) {
+        if (Objects.equals(s1, s2)) {
+            return 0;
+        } else if (s1 == null) {
+            return -1;
+        } else if (s2 == null) {
+            return 1;
+        } else if (s1.equals(s2)) {
+            return 0;
+        }
+        try {
+            return Integer.compare(Integer.parseInt(s1), Integer.parseInt(s2));
+        } catch (NumberFormatException e) {
+            return s1.compareTo(s2);
+        }
+    }
+
+    /** Generates the series ID for the other inputs than the tuner TV input. */
+    static String generateSeriesId(String packageName, String title) {
+        return packageName + "/" + title;
+    }
+
+    /** Returns ID of the program. */
+    long getId();
+
+    /** Returns the title of the program. */
+    String getTitle();
+
+    /** Returns the episode title. */
+    String getEpisodeTitle();
+
+    /** Returns the displayed title of the program episode. */
+    @Nullable
+    String getEpisodeDisplayTitle(Context context);
+
+    /**
+     * Returns the content description of the program episode, suitable for being spoken by an
+     * accessibility service.
+     */
+    String getEpisodeContentDescription(Context context);
+
+    /** Returns the description of the program. */
+    String getDescription();
+
+    /** Returns the long description of the program. */
+    String getLongDescription();
+
+    /** Returns the start time of the program in Milliseconds. */
+    long getStartTimeUtcMillis();
+
+    /** Returns the end time of the program in Milliseconds. */
+    long getEndTimeUtcMillis();
+
+    /** Returns the duration of the program in Milliseconds. */
+    long getDurationMillis();
+
+    /** Returns the series ID. */
+    @Nullable
+    String getSeriesId();
+
+    /** Returns the season number. */
+    String getSeasonNumber();
+
+    /** Returns the episode number. */
+    String getEpisodeNumber();
+
+    /** Returns URI of the program's poster. */
+    String getPosterArtUri();
+
+    /** Returns URI of the program's thumbnail. */
+    String getThumbnailUri();
+
+    /** Returns the array of the ID's of the canonical genres. */
+    int[] getCanonicalGenreIds();
+
+    /** Returns the array of content ratings. */
+    ImmutableList<TvContentRating> getContentRatings();
+
+    /** Returns channel's ID of the program. */
+    long getChannelId();
+
+    /** Returns if the program is valid. */
+    boolean isValid();
+
+    /** Checks whether the program is episodic or not. */
+    boolean isEpisodic();
+
+    /** Generates the series ID for the other inputs than the tuner TV input. */
+    class EpisodeComparator implements Comparator<BaseProgram> {
+        private final boolean mReversedSeason;
+
+        EpisodeComparator(boolean reversedSeason) {
+            mReversedSeason = reversedSeason;
+        }
+
+        @Override
+        public int compare(BaseProgram lhs, BaseProgram rhs) {
+            if (lhs == rhs) {
+                return 0;
+            }
+            int seasonNumberCompare = numberCompare(lhs.getSeasonNumber(), rhs.getSeasonNumber());
+            if (seasonNumberCompare != 0) {
+                return mReversedSeason ? -seasonNumberCompare : seasonNumberCompare;
+            } else {
+                return numberCompare(lhs.getEpisodeNumber(), rhs.getEpisodeNumber());
+            }
+        }
+    }
+}
diff --git a/src/com/android/tv/data/api/Program.java b/src/com/android/tv/data/api/Program.java
new file mode 100644
index 0000000..f2221f6
--- /dev/null
+++ b/src/com/android/tv/data/api/Program.java
@@ -0,0 +1,141 @@
+package com.android.tv.data.api;
+
+import android.content.Context;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
+
+import com.android.tv.util.images.ImageLoader;
+
+import java.io.Serializable;
+import java.util.List;
+
+/** A convenience interface to create and insert program information entries into the database. */
+public interface Program extends BaseProgram, Comparable<Program> {
+
+    /** Returns {@code true} if the program is valid and {@code false} otherwise. */
+    static boolean isProgramValid(Program program) {
+        return program != null && program.isValid();
+    }
+
+    static boolean isDuplicate(Program p1, Program p2) {
+        if (p1 == null || p2 == null) {
+            return false;
+        }
+        return p1.getChannelId() == p2.getChannelId()
+                && p1.getStartTimeUtcMillis() == p2.getStartTimeUtcMillis()
+                && p1.getEndTimeUtcMillis() == p2.getEndTimeUtcMillis();
+    }
+
+    /** True if the start or end times overlap. */
+    static boolean isOverlapping(@Nullable Program p1, @Nullable Program p2) {
+        return p1 != null
+                && p2 != null
+                && p1.getStartTimeUtcMillis() < p2.getEndTimeUtcMillis()
+                && p1.getEndTimeUtcMillis() > p2.getStartTimeUtcMillis();
+    }
+
+    /** True if the channels IDs are the same. */
+    static boolean sameChannel(@Nullable Program p1, @Nullable Program p2) {
+        return p1 != null && p2 != null && p1.getChannelId() == p2.getChannelId();
+    }
+
+    /** Returns the package name of this program. */
+    String getPackageName();
+
+    /** Returns the season title */
+    String getSeasonTitle();
+
+    /** Gets the localized duration of the program */
+    String getDurationString(Context context);
+
+    int getVideoWidth();
+
+    int getVideoHeight();
+
+    /** Returns the list of Critic Scores for this program */
+    @Nullable
+    List<CriticScore> getCriticScores();
+
+    /** Returns {@code true} if the recording of this program is prohibited. */
+    boolean isRecordingProhibited();
+
+    /** Returns array of canonical genres for this program. This is expected to be called rarely. */
+    @Nullable
+    String[] getCanonicalGenres();
+
+    /** Returns if this program has the genre. */
+    boolean hasGenre(int genreId);
+
+    /** Prefetch the program poster art. */
+    void prefetchPosterArt(Context context, int posterArtWidth, int posterArtHeight);
+
+    /**
+     * Loads the program poster art and returns it via {@code callback}.
+     *
+     * <p>Note that it may directly call {@code callback} if the program poster art already is
+     * loaded.
+     *
+     * @return {@code true} if the load is complete and the callback is executed.
+     */
+    @UiThread
+    boolean loadPosterArt(
+            Context context,
+            int posterArtWidth,
+            int posterArtHeight,
+            ImageLoader.ImageLoaderCallback<?> callback);
+
+    /** Returns a {@link Parcelable} representation of this instance. */
+    Parcelable toParcelable();
+
+    /** Holds one type of critic score and its source. */
+    final class CriticScore implements Serializable, Parcelable {
+        /** The source of the rating. */
+        public final String source;
+        /** The score. */
+        public final String score;
+        /** The url of the logo image */
+        public final String logoUrl;
+
+        public static final Creator<CriticScore> CREATOR =
+                new Creator<CriticScore>() {
+                    @Override
+                    public CriticScore createFromParcel(Parcel in) {
+                        String source = in.readString();
+                        String score = in.readString();
+                        String logoUri = in.readString();
+                        return new CriticScore(source, score, logoUri);
+                    }
+
+                    @Override
+                    public CriticScore[] newArray(int size) {
+                        return new CriticScore[size];
+                    }
+                };
+
+        /**
+         * Constructor for this class.
+         *
+         * @param source the source of the rating
+         * @param score the score
+         */
+        public CriticScore(String source, String score, String logoUrl) {
+            this.source = source;
+            this.score = score;
+            this.logoUrl = logoUrl;
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int i) {
+            out.writeString(source);
+            out.writeString(score);
+            out.writeString(logoUrl);
+        }
+    }
+}
diff --git a/src/com/android/tv/data/epg/EpgFetchHelper.java b/src/com/android/tv/data/epg/EpgFetchHelper.java
index 3843ca9..4e88911 100644
--- a/src/com/android/tv/data/epg/EpgFetchHelper.java
+++ b/src/com/android/tv/data/epg/EpgFetchHelper.java
@@ -28,12 +28,15 @@
 import android.support.annotation.WorkerThread;
 import android.text.TextUtils;
 import android.util.Log;
+
 import com.android.tv.common.CommonConstants;
 import com.android.tv.common.util.Clock;
-import com.android.tv.data.Program;
+import com.android.tv.data.ProgramImpl;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 import com.android.tv.features.TvFeatures;
 import com.android.tv.util.TvProviderUtils;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -106,7 +109,7 @@
                     ops.add(
                             ContentProviderOperation.newUpdate(
                                             TvContract.buildProgramUri(oldProgram.getId()))
-                                    .withValues(Program.toContentValues(newProgram, context))
+                                    .withValues(ProgramImpl.toContentValues(newProgram, context))
                                     .build());
                     oldProgramsIndex++;
                     newProgramsIndex++;
@@ -132,7 +135,7 @@
             if (addNewProgram) {
                 ops.add(
                         ContentProviderOperation.newInsert(Programs.CONTENT_URI)
-                                .withValues(Program.toContentValues(newProgram, context))
+                                .withValues(ProgramImpl.toContentValues(newProgram, context))
                                 .build());
             }
             // Throttle the batch operation not to cause TransactionTooLargeException.
@@ -199,7 +202,7 @@
     @WorkerThread
     private static List<Program> queryPrograms(
             Context context, long channelId, long startTimeMs, long endTimeMs) {
-        String[] projection = Program.PROJECTION;
+        String[] projection = ProgramImpl.PROJECTION;
         if (TvProviderUtils.checkSeriesIdColumn(context, Programs.CONTENT_URI)) {
             projection =
                     TvProviderUtils.addExtraColumnsToProjection(
@@ -219,7 +222,7 @@
             }
             ArrayList<Program> programs = new ArrayList<>();
             while (c.moveToNext()) {
-                programs.add(Program.fromCursor(c));
+                programs.add(ProgramImpl.fromCursor(c));
             }
             return programs;
         }
diff --git a/src/com/android/tv/data/epg/EpgFetchService.java b/src/com/android/tv/data/epg/EpgFetchService.java
index aa4f358..cfa79cb 100644
--- a/src/com/android/tv/data/epg/EpgFetchService.java
+++ b/src/com/android/tv/data/epg/EpgFetchService.java
@@ -18,22 +18,24 @@
 
 import android.app.job.JobParameters;
 import android.app.job.JobService;
+
 import com.android.tv.Starter;
-import com.android.tv.TvSingletons;
 import com.android.tv.data.ChannelDataManager;
 
+import dagger.android.AndroidInjection;
+
+import javax.inject.Inject;
+
 /** JobService to Fetch EPG data. */
 public class EpgFetchService extends JobService {
-    private EpgFetcher mEpgFetcher;
-    private ChannelDataManager mChannelDataManager;
+    @Inject EpgFetcher mEpgFetcher;
+    @Inject ChannelDataManager mChannelDataManager;
 
     @Override
     public void onCreate() {
+        AndroidInjection.inject(this);
         super.onCreate();
         Starter.start(this);
-        TvSingletons tvSingletons = TvSingletons.getSingletons(getApplicationContext());
-        mEpgFetcher = tvSingletons.getEpgFetcher();
-        mChannelDataManager = tvSingletons.getChannelDataManager();
     }
 
     @Override
diff --git a/src/com/android/tv/data/epg/EpgFetcherImpl.java b/src/com/android/tv/data/epg/EpgFetcherImpl.java
index b191421..be53099 100644
--- a/src/com/android/tv/data/epg/EpgFetcherImpl.java
+++ b/src/com/android/tv/data/epg/EpgFetcherImpl.java
@@ -38,10 +38,12 @@
 import android.support.annotation.WorkerThread;
 import android.text.TextUtils;
 import android.util.Log;
+
 import com.android.tv.TvSingletons;
 import com.android.tv.common.BuildConfig;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.buildtype.HasBuildType;
+import com.android.tv.common.dagger.annotations.ApplicationContext;
 import com.android.tv.common.util.Clock;
 import com.android.tv.common.util.CommonUtils;
 import com.android.tv.common.util.LocationUtils;
@@ -52,17 +54,20 @@
 import com.android.tv.data.ChannelImpl;
 import com.android.tv.data.ChannelLogoFetcher;
 import com.android.tv.data.Lineup;
-import com.android.tv.data.Program;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 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;
@@ -73,6 +78,8 @@
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
+import javax.inject.Inject;
+
 /**
  * The service class to fetch EPG routinely or on-demand during channel scanning
  *
@@ -130,31 +137,9 @@
 
     private Clock mClock;
 
-    public static EpgFetcher create(Context context) {
-        context = context.getApplicationContext();
-        TvSingletons tvSingletons = TvSingletons.getSingletons(context);
-        ChannelDataManager channelDataManager = tvSingletons.getChannelDataManager();
-        PerformanceMonitor performanceMonitor = tvSingletons.getPerformanceMonitor();
-        EpgReader epgReader = tvSingletons.providesEpgReader().get();
-        Clock clock = tvSingletons.getClock();
-        EpgInputWhiteList epgInputWhiteList =
-                new EpgInputWhiteList(tvSingletons.getCloudEpgFlags());
-        BackendKnobsFlags backendKnobsFlags = tvSingletons.getBackendKnobs();
-        HasBuildType.BuildType buildType = tvSingletons.getBuildType();
-        return new EpgFetcherImpl(
-                context,
-                epgInputWhiteList,
-                channelDataManager,
-                epgReader,
-                performanceMonitor,
-                clock,
-                backendKnobsFlags,
-                buildType);
-    }
-
-    @VisibleForTesting
-    EpgFetcherImpl(
-            Context context,
+    @Inject
+    public EpgFetcherImpl(
+            @ApplicationContext Context context,
             EpgInputWhiteList epgInputWhiteList,
             ChannelDataManager channelDataManager,
             EpgReader epgReader,
@@ -485,7 +470,7 @@
     @WorkerThread
     private void batchUpdateEpg(Map<EpgReader.EpgChannel, Collection<Program>> allPrograms) {
         for (Map.Entry<EpgReader.EpgChannel, Collection<Program>> entry : allPrograms.entrySet()) {
-            List<Program> programs = new ArrayList(entry.getValue());
+            List<Program> programs = new ArrayList<>(entry.getValue());
             if (programs == null) {
                 continue;
             }
@@ -604,6 +589,7 @@
                             ? ((Integer) REASON_CLOUD_EPG_FAILURE)
                             : anyCloudEpgSuccess ? null : builtInResult;
                 }
+                clearUnusedLineups(null);
                 return builtInResult;
             } finally {
                 TrafficStats.setThreadStatsTag(oldTag);
diff --git a/src/com/android/tv/data/epg/EpgInputWhiteList.java b/src/com/android/tv/data/epg/EpgInputWhiteList.java
index 24b4fe3..4a5f98b 100644
--- a/src/com/android/tv/data/epg/EpgInputWhiteList.java
+++ b/src/com/android/tv/data/epg/EpgInputWhiteList.java
@@ -21,13 +21,14 @@
 import android.text.TextUtils;
 import android.util.Log;
 import com.android.tv.common.BuildConfig;
-import com.android.tv.common.experiments.Experiments;
 import com.android.tv.common.flags.CloudEpgFlags;
+import com.android.tv.common.flags.LegacyFlags;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import javax.inject.Inject;
 
 /** Checks if a package or a input is white listed. */
 public final class EpgInputWhiteList {
@@ -36,6 +37,7 @@
     private static final String QA_DEV_INPUTS =
             "com.example.partnersupportsampletvinput/.SampleTvInputService,"
                     + "com.android.tv.tuner.sample.dvb/.tvinput.SampleDvbTunerTvInputService";
+    private final LegacyFlags mLegacyFlags;
 
     /** Returns the package portion of a inputId */
     @Nullable
@@ -43,10 +45,12 @@
         return inputId == null ? null : inputId.substring(0, inputId.indexOf("/"));
     }
 
-    private final CloudEpgFlags cloudEpgFlags;
+    private final CloudEpgFlags mCloudEpgFlags;
 
-    public EpgInputWhiteList(CloudEpgFlags cloudEpgFlags) {
-        this.cloudEpgFlags = cloudEpgFlags;
+    @Inject
+    public EpgInputWhiteList(CloudEpgFlags cloudEpgFlags, LegacyFlags legacyFlags) {
+        mCloudEpgFlags = cloudEpgFlags;
+        mLegacyFlags = legacyFlags;
     }
 
     public boolean isInputWhiteListed(String inputId) {
@@ -71,8 +75,8 @@
     }
 
     private Set<String> getWhiteListedInputs() {
-        Set<String> result = toInputSet(cloudEpgFlags.thirdPartyEpgInputsCsv());
-        if (BuildConfig.ENG || Experiments.ENABLE_QA_FEATURES.get()) {
+        Set<String> result = toInputSet(mCloudEpgFlags.thirdPartyEpgInputsCsv());
+        if (BuildConfig.ENG || mLegacyFlags.enableQaFeatures()) {
             HashSet<String> moreInputs = new HashSet<>(toInputSet(QA_DEV_INPUTS));
             if (result.isEmpty()) {
                 result = moreInputs;
diff --git a/src/com/android/tv/data/epg/EpgReader.java b/src/com/android/tv/data/epg/EpgReader.java
index c9fcd97..8c0e3f0 100644
--- a/src/com/android/tv/data/epg/EpgReader.java
+++ b/src/com/android/tv/data/epg/EpgReader.java
@@ -19,11 +19,14 @@
 import android.support.annotation.AnyThread;
 import android.support.annotation.NonNull;
 import android.support.annotation.WorkerThread;
+
 import com.android.tv.data.Lineup;
-import com.android.tv.data.Program;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 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;
@@ -36,8 +39,8 @@
     /** Value class that holds a EpgChannelId and its corresponding {@link Channel} */
     @AutoValue
     abstract class EpgChannel {
-        public static EpgChannel createEpgChannel(Channel channel, String epgChannelId,
-                boolean dbUpdateNeeded) {
+        public static EpgChannel createEpgChannel(
+                Channel channel, String epgChannelId, boolean dbUpdateNeeded) {
             return new AutoValue_EpgReader_EpgChannel(channel, epgChannelId, dbUpdateNeeded);
         }
 
diff --git a/src/com/android/tv/data/epg/StubEpgReader.java b/src/com/android/tv/data/epg/StubEpgReader.java
index 3b00148..19bf786 100644
--- a/src/com/android/tv/data/epg/StubEpgReader.java
+++ b/src/com/android/tv/data/epg/StubEpgReader.java
@@ -16,21 +16,25 @@
 
 package com.android.tv.data.epg;
 
-import android.content.Context;
 import android.support.annotation.NonNull;
+
 import com.android.tv.data.Lineup;
-import com.android.tv.data.Program;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 import com.android.tv.dvr.data.SeriesInfo;
+
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import javax.inject.Inject;
+
 /** A stub class to read EPG. */
 public class StubEpgReader implements EpgReader {
-    public StubEpgReader(@SuppressWarnings("unused") Context context) {}
+    @Inject
+    public StubEpgReader() {}
 
     @Override
     public boolean isAvailable() {
diff --git a/src/com/android/tv/dialog/PinDialogFragment.java b/src/com/android/tv/dialog/PinDialogFragment.java
index 8730809..c714558 100644
--- a/src/com/android/tv/dialog/PinDialogFragment.java
+++ b/src/com/android/tv/dialog/PinDialogFragment.java
@@ -18,6 +18,7 @@
 
 import android.app.ActivityManager;
 import android.app.Dialog;
+import android.content.Context;
 import android.content.DialogInterface;
 import android.content.SharedPreferences;
 import android.media.tv.TvContentRating;
@@ -33,10 +34,13 @@
 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.dialog.picker.TvPinPicker;
+import com.android.tv.util.TvInputManagerHelper;
 import com.android.tv.util.TvSettings;
+import dagger.android.AndroidInjection;
+import com.android.tv.common.flags.UiFlags;
+import javax.inject.Inject;
 
 public class PinDialogFragment extends SafeDismissDialogFragment {
     private static final String TAG = "PinDialogFragment";
@@ -80,7 +84,8 @@
     private TextView mWrongPinView;
     private View mEnterPinView;
     private TextView mTitleView;
-    private PinPicker mPicker;
+
+    private TvPinPicker mTvPinPicker;
     private SharedPreferences mSharedPreferences;
     private String mPrevPin;
     private String mPin;
@@ -88,6 +93,8 @@
     private int mWrongPinCount;
     private long mDisablePinUntil;
     private final Handler mHandler = new Handler();
+    @Inject TvInputManagerHelper mTvInputManagerHelper;
+    @Inject UiFlags mUiFlags;
 
     public static PinDialogFragment create(int type) {
         return create(type, null);
@@ -103,6 +110,12 @@
     }
 
     @Override
+    public void onAttach(Context context) {
+        AndroidInjection.inject(this);
+        super.onAttach(context);
+    }
+
+    @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         mRequestType = getArguments().getInt(ARGS_TYPE, PIN_DIALOG_TYPE_ENTER_PIN);
@@ -154,8 +167,8 @@
         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(
+        mTvPinPicker = v.findViewById(R.id.tv_pin_picker);
+        mTvPinPicker.setOnClickListener(
                 view -> {
                     String pin = getPinInput();
                     if (!TextUtils.isEmpty(pin)) {
@@ -183,8 +196,7 @@
                     mTitleView.setText(
                             getString(
                                     R.string.pin_enter_unlock_dvr,
-                                    TvSingletons.getSingletons(getContext())
-                                            .getTvInputManagerHelper()
+                                    mTvInputManagerHelper
                                             .getContentRatingsManager()
                                             .getDisplayNameForRating(tvContentRating)));
                 }
@@ -204,7 +216,8 @@
         if (mType != PIN_DIALOG_TYPE_NEW_PIN) {
             updateWrongPin();
         }
-        mPicker.requestFocus();
+
+        mTvPinPicker.requestFocus();
         return v;
     }
 
@@ -338,11 +351,11 @@
     }
 
     private String getPinInput() {
-        return mPicker.getPinInput();
+        return mTvPinPicker.getPin();
     }
 
     private void resetPinInput() {
-        mPicker.resetPinInput();
+        mTvPinPicker.resetPin();
     }
 
     /**
diff --git a/src/com/android/tv/dialog/picker/PinPicker.java b/src/com/android/tv/dialog/picker/PinPicker.java
deleted file mode 100644
index f501dfd..0000000
--- a/src/com/android/tv/dialog/picker/PinPicker.java
+++ /dev/null
@@ -1,131 +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.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/dialog/picker/TvPinPicker.java b/src/com/android/tv/dialog/picker/TvPinPicker.java
new file mode 100644
index 0000000..064b7f0
--- /dev/null
+++ b/src/com/android/tv/dialog/picker/TvPinPicker.java
@@ -0,0 +1,54 @@
+/*
+ * 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.dialog.picker;
+
+import static android.content.Context.ACCESSIBILITY_SERVICE;
+
+import android.content.Context;
+import androidx.leanback.widget.picker.PinPicker;
+import android.util.AttributeSet;
+import android.view.accessibility.AccessibilityManager;
+
+/** 4 digit picker */
+public final class TvPinPicker extends PinPicker {
+
+    private boolean mSkipPerformClick = true;
+    private boolean mIsAccessibilityEnabled = false;
+
+    public TvPinPicker(Context context, AttributeSet attributeSet) {
+        this(context, attributeSet, 0);
+    }
+
+    public TvPinPicker(Context context, AttributeSet attributeSet, int defStyleAttr) {
+        super(context, attributeSet, defStyleAttr);
+        setActivated(true);
+        AccessibilityManager am =
+                (AccessibilityManager) context.getSystemService(ACCESSIBILITY_SERVICE);
+        mIsAccessibilityEnabled = am.isEnabled();
+    }
+
+    @Override
+    public boolean performClick() {
+        // (b/120096347) Skip first click when talkback is enabled
+        if (mSkipPerformClick && mIsAccessibilityEnabled) {
+            mSkipPerformClick = false;
+            /* Force focus to next value */
+            setColumnValue(getSelectedColumn(), 1, true);
+            return false;
+        }
+        return super.performClick();
+    }
+}
diff --git a/src/com/android/tv/dvr/DvrDataManagerImpl.java b/src/com/android/tv/dvr/DvrDataManagerImpl.java
index 0053650..3e26a23 100644
--- a/src/com/android/tv/dvr/DvrDataManagerImpl.java
+++ b/src/com/android/tv/dvr/DvrDataManagerImpl.java
@@ -37,8 +37,10 @@
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.Range;
+
 import com.android.tv.TvSingletons;
 import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.dagger.annotations.ApplicationContext;
 import com.android.tv.common.recording.RecordingStorageStatusManager;
 import com.android.tv.common.recording.RecordingStorageStatusManager.OnStorageMountChangedListener;
 import com.android.tv.common.util.Clock;
@@ -48,6 +50,7 @@
 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.DvrDatabaseHelper;
 import com.android.tv.dvr.provider.DvrDbFuture.AddScheduleFuture;
 import com.android.tv.dvr.provider.DvrDbFuture.AddSeriesRecordingFuture;
 import com.android.tv.dvr.provider.DvrDbFuture.DeleteScheduleFuture;
@@ -60,11 +63,14 @@
 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.AsyncDbTask.DbExecutor;
 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;
@@ -76,9 +82,13 @@
 import java.util.concurrent.Executor;
 import java.util.concurrent.Future;
 
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
 /** DVR Data manager to handle recordings and schedules. */
 @MainThread
 @TargetApi(Build.VERSION_CODES.N)
+@Singleton
 public class DvrDataManagerImpl extends BaseDvrDataManager {
     private static final String TAG = "DvrDataManagerImpl";
     private static final boolean DEBUG = false;
@@ -98,6 +108,7 @@
     private final HashMap<Long, SeriesRecording> mSeriesRecordingsForRemovedInput = new HashMap<>();
 
     private final Context mContext;
+    private final DvrDatabaseHelper mDbHelper;
     private Executor mDbExecutor;
     private final ContentObserver mContentObserver =
             new ContentObserver(new Handler(Looper.getMainLooper())) {
@@ -187,20 +198,28 @@
         return moved;
     }
 
-    public DvrDataManagerImpl(Context context, Clock clock) {
+    @Inject
+    public DvrDataManagerImpl(
+            @ApplicationContext Context context,
+            Clock clock,
+            TvInputManagerHelper tvInputManagerHelper,
+            @DbExecutor Executor dbExecutor,
+            DvrDatabaseHelper dbHelper) {
         super(context, clock);
         mContext = context;
         TvSingletons tvSingletons = TvSingletons.getSingletons(context);
-        mInputManager = tvSingletons.getTvInputManagerHelper();
+        mInputManager = tvInputManagerHelper;
         mStorageStatusManager = tvSingletons.getRecordingStorageStatusManager();
-        mDbExecutor = tvSingletons.getDbExecutor();
+        mDbExecutor = dbExecutor;
+        mDbHelper = dbHelper;
+        start();
     }
 
-    public void start() {
+    private void start() {
         mInputManager.addCallback(mInputCallback);
         mStorageStatusManager.addListener(mStorageMountChangedListener);
         DvrQuerySeriesRecordingFuture dvrQuerySeriesRecordingTask =
-                new DvrQuerySeriesRecordingFuture(mContext);
+                new DvrQuerySeriesRecordingFuture(mDbHelper);
         ListenableFuture<List<SeriesRecording>> dvrQuerySeriesRecordingFuture =
                 dvrQuerySeriesRecordingTask.executeOnDbThread(
                         new FutureCallback<List<SeriesRecording>>() {
@@ -213,7 +232,8 @@
                                     if (SoftPreconditions.checkState(
                                             !seriesIds.contains(r.getSeriesId()),
                                             TAG,
-                                            "Skip loading series recording with duplicate series ID: "
+                                            "Skip loading series recording with duplicate series"
+                                                    + " ID: "
                                                     + r)) {
                                         seriesIds.add(r.getSeriesId());
                                         if (isInputAvailable(r.getInputId())) {
@@ -237,7 +257,7 @@
                             }
                         });
         mPendingDvrFuture.add(dvrQuerySeriesRecordingFuture);
-        DvrQueryScheduleFuture dvrQueryScheduleTask = new DvrQueryScheduleFuture(mContext);
+        DvrQueryScheduleFuture dvrQueryScheduleTask = new DvrQueryScheduleFuture(mDbHelper);
         ListenableFuture<List<ScheduledRecording>> dvrQueryScheduleFuture =
                 dvrQueryScheduleTask.executeOnDbThread(
                         new FutureCallback<List<ScheduledRecording>>() {
@@ -641,7 +661,7 @@
             notifyScheduledRecordingAdded(schedules);
         }
         ListenableFuture addScheduleFuture =
-                new AddScheduleFuture(mContext)
+                new AddScheduleFuture(mDbHelper)
                         .executeOnDbThread(removeFromSetOnCompletion, schedules);
         mNoStopFuture.add(addScheduleFuture);
         removeDeletedSchedules(schedules);
@@ -663,7 +683,7 @@
             notifySeriesRecordingAdded(seriesRecordings);
         }
         ListenableFuture addSeriesRecordingFuture =
-                new AddSeriesRecordingFuture(mContext)
+                new AddSeriesRecordingFuture(mDbHelper)
                         .executeOnDbThread(removeFromSetOnCompletion, seriesRecordings);
         mNoStopFuture.add(addSeriesRecordingFuture);
     }
@@ -723,7 +743,7 @@
         }
         if (!schedulesToDelete.isEmpty()) {
             ListenableFuture deleteScheduleFuture =
-                    new DeleteScheduleFuture(mContext)
+                    new DeleteScheduleFuture(mDbHelper)
                             .executeOnDbThread(
                                     removeFromSetOnCompletion,
                                     ScheduledRecording.toArray(schedulesToDelete));
@@ -731,7 +751,7 @@
         }
         if (!schedulesNotToDelete.isEmpty()) {
             ListenableFuture updateScheduleFuture =
-                    new UpdateScheduleFuture(mContext)
+                    new UpdateScheduleFuture(mDbHelper)
                             .executeOnDbThread(
                                     removeFromSetOnCompletion,
                                     ScheduledRecording.toArray(schedulesNotToDelete));
@@ -774,7 +794,7 @@
             notifySeriesRecordingRemoved(seriesRecordings);
         }
         ListenableFuture deleteSeriesRecordingFuture =
-                new DeleteSeriesRecordingFuture(mContext)
+                new DeleteSeriesRecordingFuture(mDbHelper)
                         .executeOnDbThread(removeFromSetOnCompletion, seriesRecordings);
         mNoStopFuture.add(deleteSeriesRecordingFuture);
         removeDeletedSchedules(seriesRecordings);
@@ -829,7 +849,7 @@
         }
         if (updateDb) {
             ListenableFuture updateScheduleFuture =
-                    new UpdateScheduleFuture(mContext)
+                    new UpdateScheduleFuture(mDbHelper)
                             .executeOnDbThread(removeFromSetOnCompletion, scheduleArray);
             mNoStopFuture.add(updateScheduleFuture);
         }
@@ -856,7 +876,7 @@
             notifySeriesRecordingChanged(seriesRecordings);
         }
         ListenableFuture updateSeriesRecordingFuture =
-                new UpdateSeriesRecordingFuture(mContext)
+                new UpdateSeriesRecordingFuture(mDbHelper)
                         .executeOnDbThread(removeFromSetOnCompletion, seriesRecordings);
         mNoStopFuture.add(updateSeriesRecordingFuture);
     }
@@ -877,7 +897,7 @@
         }
         if (!schedulesToDelete.isEmpty()) {
             ListenableFuture deleteScheduleFuture =
-                    new DeleteScheduleFuture(mContext)
+                    new DeleteScheduleFuture(mDbHelper)
                             .executeOnDbThread(
                                     removeFromSetOnCompletion,
                                     ScheduledRecording.toArray(schedulesToDelete));
@@ -902,7 +922,7 @@
         }
         if (!schedulesToDelete.isEmpty()) {
             ListenableFuture deleteScheduleFuture =
-                    new DeleteScheduleFuture(mContext)
+                    new DeleteScheduleFuture(mDbHelper)
                             .executeOnDbThread(
                                     removeFromSetOnCompletion,
                                     ScheduledRecording.toArray(schedulesToDelete));
@@ -950,7 +970,7 @@
             mSeriesRecordingsForRemovedInput.remove(r.getId());
         }
         ListenableFuture deleteSeriesRecordingFuture =
-                new DeleteSeriesRecordingFuture(mContext)
+                new DeleteSeriesRecordingFuture(mDbHelper)
                         .executeOnDbThread(
                                 removeFromSetOnCompletion,
                                 SeriesRecording.toArray(removedSeriesRecordings));
@@ -1043,13 +1063,13 @@
             }
         }
         ListenableFuture deleteScheduleFuture =
-                new DeleteScheduleFuture(mContext)
+                new DeleteScheduleFuture(mDbHelper)
                         .executeOnDbThread(
                                 removeFromSetOnCompletion,
                                 ScheduledRecording.toArray(schedulesToDelete));
         mNoStopFuture.add(deleteScheduleFuture);
         ListenableFuture deleteSeriesRecordingFuture =
-                new DeleteSeriesRecordingFuture(mContext)
+                new DeleteSeriesRecordingFuture(mDbHelper)
                         .executeOnDbThread(
                                 removeFromSetOnCompletion,
                                 SeriesRecording.toArray(seriesRecordingsToDelete));
@@ -1085,7 +1105,7 @@
         if (!removedSeriesRecordings.isEmpty()) {
             SeriesRecording[] removed = SeriesRecording.toArray(removedSeriesRecordings);
             ListenableFuture deleteSeriesRecordingFuture =
-                    new DeleteSeriesRecordingFuture(mContext)
+                    new DeleteSeriesRecordingFuture(mDbHelper)
                             .executeOnDbThread(removeFromSetOnCompletion, removed);
             mNoStopFuture.add(deleteSeriesRecordingFuture);
             if (mDvrLoadFinished) {
diff --git a/src/com/android/tv/dvr/DvrManager.java b/src/com/android/tv/dvr/DvrManager.java
index cc9ad37..12982c6 100644
--- a/src/com/android/tv/dvr/DvrManager.java
+++ b/src/com/android/tv/dvr/DvrManager.java
@@ -37,12 +37,13 @@
 import android.support.annotation.WorkerThread;
 import android.util.Log;
 import android.util.Range;
+
 import com.android.tv.TvSingletons;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.feature.CommonFeatures;
 import com.android.tv.common.util.CommonUtils;
-import com.android.tv.data.Program;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener;
 import com.android.tv.dvr.DvrDataManager.RecordedProgramListener;
 import com.android.tv.dvr.DvrScheduleManager.OnInitializeListener;
@@ -51,6 +52,7 @@
 import com.android.tv.dvr.data.SeriesRecording;
 import com.android.tv.util.AsyncDbTask;
 import com.android.tv.util.Utils;
+
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -187,7 +189,7 @@
                         ? mScheduleManager.suggestNewPriority()
                         : mScheduleManager.suggestHighestPriority(
                                 seriesRecording.getInputId(),
-                                new Range(
+                                Range.create(
                                         program.getStartTimeUtcMillis(),
                                         program.getEndTimeUtcMillis()),
                                 seriesRecording.getPriority()));
diff --git a/src/com/android/tv/dvr/DvrScheduleManager.java b/src/com/android/tv/dvr/DvrScheduleManager.java
index 7202dce..3afcc42 100644
--- a/src/com/android/tv/dvr/DvrScheduleManager.java
+++ b/src/com/android/tv/dvr/DvrScheduleManager.java
@@ -25,11 +25,12 @@
 import android.support.annotation.VisibleForTesting;
 import android.util.ArraySet;
 import android.util.Range;
+
 import com.android.tv.TvSingletons;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.data.ChannelDataManager;
-import com.android.tv.data.Program;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener;
 import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
 import com.android.tv.dvr.data.ScheduledRecording;
@@ -37,6 +38,7 @@
 import com.android.tv.dvr.recorder.InputTaskScheduler;
 import com.android.tv.util.CompositeComparator;
 import com.android.tv.util.Utils;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -76,7 +78,7 @@
                     ScheduledRecording.ID_COMPARATOR);
 
     private final Context mContext;
-    private final DvrDataManagerImpl mDataManager;
+    private final DvrDataManager mDataManager;
     private final ChannelDataManager mChannelDataManager;
 
     private final Map<String, List<ScheduledRecording>> mInputScheduleMap = new HashMap<>();
@@ -95,7 +97,7 @@
     public DvrScheduleManager(Context context) {
         mContext = context;
         TvSingletons tvSingletons = TvSingletons.getSingletons(context);
-        mDataManager = (DvrDataManagerImpl) tvSingletons.getDvrDataManager();
+        mDataManager = tvSingletons.getDvrDataManager();
         mChannelDataManager = tvSingletons.getChannelDataManager();
         if (mDataManager.isDvrScheduleLoadFinished() && mChannelDataManager.isDbLoadFinished()) {
             buildData();
diff --git a/src/com/android/tv/dvr/DvrStorageStatusManager.java b/src/com/android/tv/dvr/DvrStorageStatusManager.java
index dc347a9..a0ae893 100644
--- a/src/com/android/tv/dvr/DvrStorageStatusManager.java
+++ b/src/com/android/tv/dvr/DvrStorageStatusManager.java
@@ -114,7 +114,7 @@
                     return;
                 }
                 for (TvInputInfo info : tvInputInfoList) {
-                    if (CommonUtils.isBundledInput(info.getId())) {
+                    if (CommonUtils.isBundledInput(info.getId()) && dvrManager != null) {
                         dvrManager.forgetStorage(info.getId());
                     }
                 }
diff --git a/src/com/android/tv/dvr/DvrWatchedPositionManager.java b/src/com/android/tv/dvr/DvrWatchedPositionManager.java
index 8616962..b4fea7f 100644
--- a/src/com/android/tv/dvr/DvrWatchedPositionManager.java
+++ b/src/com/android/tv/dvr/DvrWatchedPositionManager.java
@@ -20,8 +20,10 @@
 import android.content.SharedPreferences;
 import android.media.tv.TvInputManager;
 import android.support.annotation.IntDef;
+
 import com.android.tv.common.util.SharedPreferencesUtils;
 import com.android.tv.dvr.data.RecordedProgram;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
@@ -36,7 +38,7 @@
  */
 public class DvrWatchedPositionManager {
     private SharedPreferences mWatchedPositions;
-    private final Map<Long, Set> mListeners = new HashMap<>();
+    private final Map<Long, Set<WatchedPositionChangedListener>> mListeners = new HashMap<>();
 
     /**
      * The minimum percentage of recorded program being watched that will be considered as being
diff --git a/src/com/android/tv/dvr/data/RecordedProgram.java b/src/com/android/tv/dvr/data/RecordedProgram.java
index 899e65a..6143055 100644
--- a/src/com/android/tv/dvr/data/RecordedProgram.java
+++ b/src/com/android/tv/dvr/data/RecordedProgram.java
@@ -36,9 +36,10 @@
 import com.android.tv.common.data.RecordedProgramState;
 import com.android.tv.common.util.CommonUtils;
 import com.android.tv.common.util.StringUtils;
-import com.android.tv.data.BaseProgram;
+import com.android.tv.data.BaseProgramImpl;
 import com.android.tv.data.GenreItems;
 import com.android.tv.data.InternalDataUtils;
+import com.android.tv.data.api.BaseProgram;
 import com.android.tv.util.TvProviderUtils;
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableList;
@@ -49,7 +50,7 @@
 /** Immutable instance of {@link android.media.tv.TvContract.RecordedPrograms}. */
 @TargetApi(Build.VERSION_CODES.N)
 @AutoValue
-public abstract class RecordedProgram extends BaseProgram {
+public abstract class RecordedProgram extends BaseProgramImpl {
     public static final int ID_NOT_SET = -1;
     private static final String TAG = "RecordedProgram";
 
diff --git a/src/com/android/tv/dvr/data/ScheduledRecording.java b/src/com/android/tv/dvr/data/ScheduledRecording.java
index ba6d3cf..1237fb3 100644
--- a/src/com/android/tv/dvr/data/ScheduledRecording.java
+++ b/src/com/android/tv/dvr/data/ScheduledRecording.java
@@ -27,15 +27,17 @@
 import android.support.annotation.Nullable;
 import android.text.TextUtils;
 import android.util.Range;
+
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.util.CommonUtils;
-import com.android.tv.data.Program;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 import com.android.tv.dvr.DvrScheduleManager;
 import com.android.tv.dvr.provider.DvrContract.Schedules;
 import com.android.tv.util.CompositeComparator;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.Collection;
@@ -492,7 +494,7 @@
                 }
             };
 
-    /** The ID internal to Live TV */
+    /** The ID internal to TV app */
     private long mId;
 
     /**
diff --git a/src/com/android/tv/dvr/data/SeriesRecording.java b/src/com/android/tv/dvr/data/SeriesRecording.java
index 6cb0e83..cd7d966 100644
--- a/src/com/android/tv/dvr/data/SeriesRecording.java
+++ b/src/com/android/tv/dvr/data/SeriesRecording.java
@@ -22,11 +22,14 @@
 import android.os.Parcelable;
 import android.support.annotation.IntDef;
 import android.text.TextUtils;
-import com.android.tv.data.BaseProgram;
-import com.android.tv.data.Program;
+
+import com.android.tv.data.BaseProgramImpl;
+import com.android.tv.data.api.BaseProgram;
+import com.android.tv.data.api.Program;
 import com.android.tv.dvr.DvrScheduleManager;
 import com.android.tv.dvr.provider.DvrContract.SeriesRecordings;
 import com.android.tv.util.Utils;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.Arrays;
@@ -85,7 +88,8 @@
             (SeriesRecording lhs, SeriesRecording rhs) -> Long.compare(lhs.mId, rhs.mId);
 
     /**
-     * Creates a new Builder with the values set from the series information of {@link BaseProgram}.
+     * Creates a new Builder with the values set from the series information of {@link
+     * BaseProgramImpl}.
      */
     public static Builder builder(String inputId, BaseProgram p) {
         return new Builder()
diff --git a/src/com/android/tv/dvr/provider/DvrContract.java b/src/com/android/tv/dvr/provider/DvrContract.java
index a5f2e2c..8539ae3 100644
--- a/src/com/android/tv/dvr/provider/DvrContract.java
+++ b/src/com/android/tv/dvr/provider/DvrContract.java
@@ -20,7 +20,7 @@
 
 /**
  * The contract between the DVR provider and applications. Contains definitions for the supported
- * columns. It's for the internal use in Live TV.
+ * columns. It's for the internal use in TV app.
  */
 public final class DvrContract {
     /** Column definition for Schedules table. */
@@ -69,8 +69,8 @@
         public static final String FAILED_REASON_INVALID_CHANNEL = "FAILED_REASON_INVALID_CHANNEL";
 
         /** The recording failed because the scheduler was stopped */
-        public static final String FAILED_REASON_SCHEDULER_STOPPED
-                = "FAILED_REASON_SCHEDULER_STOPPED";
+        public static final String FAILED_REASON_SCHEDULER_STOPPED =
+                "FAILED_REASON_SCHEDULER_STOPPED";
 
         /** The recording failed because some messages were not sent to the message queue */
         public static final String FAILED_REASON_MESSAGE_NOT_SENT =
@@ -84,8 +84,7 @@
                 "FAILED_REASON_CONNECTION_FAILED";
 
         /**
-         * The recording failed because a required recording resource was not able to be
-         * allocated.
+         * The recording failed because a required recording resource was not able to be allocated.
          */
         public static final String FAILED_REASON_RESOURCE_BUSY = "FAILED_REASON_RESOURCE_BUSY";
 
diff --git a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
index ebf133d..1dcda8e 100644
--- a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
+++ b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
@@ -26,12 +26,18 @@
 import android.provider.BaseColumns;
 import android.text.TextUtils;
 import android.util.Log;
+
+import com.android.tv.common.dagger.annotations.ApplicationContext;
 import com.android.tv.dvr.data.ScheduledRecording;
 import com.android.tv.dvr.data.SeriesRecording;
 import com.android.tv.dvr.provider.DvrContract.Schedules;
 import com.android.tv.dvr.provider.DvrContract.SeriesRecordings;
 
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
 /** A data class for one recorded contents. */
+@Singleton
 public class DvrDatabaseHelper extends SQLiteOpenHelper {
     private static final String TAG = "DvrDatabaseHelper";
     private static final boolean DEBUG = false;
@@ -238,8 +244,9 @@
         return "DELETE FROM " + tableName + " WHERE " + BaseColumns._ID + "=?";
     }
 
-    public DvrDatabaseHelper(Context context) {
-        super(context.getApplicationContext(), DB_NAME, null, DATABASE_VERSION);
+    @Inject
+    public DvrDatabaseHelper(@ApplicationContext Context context) {
+        super(context, DB_NAME, null, DATABASE_VERSION);
     }
 
     @Override
@@ -266,8 +273,12 @@
             return;
         }
         if (oldVersion < 18) {
-            db.execSQL("ALTER TABLE " + Schedules.TABLE_NAME + " ADD COLUMN "
-                    + Schedules.COLUMN_FAILED_REASON + " TEXT DEFAULT null;");
+            db.execSQL(
+                    "ALTER TABLE "
+                            + Schedules.TABLE_NAME
+                            + " ADD COLUMN "
+                            + Schedules.COLUMN_FAILED_REASON
+                            + " TEXT DEFAULT null;");
         }
     }
 
diff --git a/src/com/android/tv/dvr/provider/DvrDbFuture.java b/src/com/android/tv/dvr/provider/DvrDbFuture.java
index ae8c480..cbc2c07 100644
--- a/src/com/android/tv/dvr/provider/DvrDbFuture.java
+++ b/src/com/android/tv/dvr/provider/DvrDbFuture.java
@@ -16,21 +16,23 @@
 
 package com.android.tv.dvr.provider;
 
-import android.content.Context;
 import android.database.Cursor;
 import android.support.annotation.Nullable;
 import android.util.Log;
+
 import com.android.tv.common.concurrent.NamedThreadFactory;
 import com.android.tv.dvr.data.ScheduledRecording;
 import com.android.tv.dvr.data.SeriesRecording;
 import com.android.tv.dvr.provider.DvrContract.Schedules;
 import com.android.tv.dvr.provider.DvrContract.SeriesRecordings;
 import com.android.tv.util.MainThreadExecutor;
+
 import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Executors;
@@ -38,29 +40,24 @@
 /** {@link DvrDbFuture} that defaults to executing on its own single threaded Executor Service. */
 public abstract class DvrDbFuture<ParamsT, ResultT> {
     private static final NamedThreadFactory THREAD_FACTORY =
-        new NamedThreadFactory(DvrDbFuture.class.getSimpleName());
+            new NamedThreadFactory(DvrDbFuture.class.getSimpleName());
     private static final ListeningExecutorService DB_EXECUTOR =
-        MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(THREAD_FACTORY));
+            MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(THREAD_FACTORY));
 
-    private static DvrDatabaseHelper sDbHelper;
+    final DvrDatabaseHelper mDbHelper;
     private ListenableFuture<ResultT> mFuture;
 
-    final Context mContext;
-
-    private DvrDbFuture(Context context) {
-        mContext = context;
+    private DvrDbFuture(DvrDatabaseHelper mDbHelper) {
+        this.mDbHelper = mDbHelper;
     }
 
-    /** Execute the task on the {@link #DB_EXECUTOR} thread and return Future*/
+    /** Execute the task on the {@link #DB_EXECUTOR} thread and return Future */
     @SafeVarargs
     public final ListenableFuture<ResultT> executeOnDbThread(
-        FutureCallback<ResultT> callback, ParamsT... params) {
-            if (sDbHelper == null) {
-                sDbHelper = new DvrDatabaseHelper(mContext.getApplicationContext());
-            }
-            mFuture = DB_EXECUTOR.submit(() -> dbHelperInBackground(params));
-            Futures.addCallback(mFuture, callback, MainThreadExecutor.getInstance());
-            return mFuture;
+            FutureCallback<ResultT> callback, ParamsT... params) {
+        mFuture = DB_EXECUTOR.submit(() -> dbHelperInBackground(params));
+        Futures.addCallback(mFuture, callback, MainThreadExecutor.getInstance());
+        return mFuture;
     }
 
     /** Executes in the background after initializing DbHelper} */
@@ -72,52 +69,48 @@
     }
 
     /** Inserts schedules. */
-    public static class AddScheduleFuture
-            extends DvrDbFuture<ScheduledRecording, Void> {
-        public AddScheduleFuture(Context context) {
-            super(context);
+    public static class AddScheduleFuture extends DvrDbFuture<ScheduledRecording, Void> {
+        public AddScheduleFuture(DvrDatabaseHelper dbHelper) {
+            super(dbHelper);
         }
 
         @Override
         protected final Void dbHelperInBackground(ScheduledRecording... params) {
-            sDbHelper.insertSchedules(params);
+            mDbHelper.insertSchedules(params);
             return null;
         }
     }
 
     /** Update schedules. */
-    public static class UpdateScheduleFuture
-            extends DvrDbFuture<ScheduledRecording, Void> {
-        public UpdateScheduleFuture(Context context) {
-            super(context);
+    public static class UpdateScheduleFuture extends DvrDbFuture<ScheduledRecording, Void> {
+        public UpdateScheduleFuture(DvrDatabaseHelper dbHelper) {
+            super(dbHelper);
         }
 
         @Override
         protected final Void dbHelperInBackground(ScheduledRecording... params) {
-            sDbHelper.updateSchedules(params);
+            mDbHelper.updateSchedules(params);
             return null;
         }
     }
 
     /** Delete schedules. */
-    public static class DeleteScheduleFuture
-            extends DvrDbFuture<ScheduledRecording, Void> {
-        public DeleteScheduleFuture(Context context) {
-            super(context);
+    public static class DeleteScheduleFuture extends DvrDbFuture<ScheduledRecording, Void> {
+        public DeleteScheduleFuture(DvrDatabaseHelper dbHelper) {
+            super(dbHelper);
         }
 
         @Override
         protected final Void dbHelperInBackground(ScheduledRecording... params) {
-            sDbHelper.deleteSchedules(params);
+            mDbHelper.deleteSchedules(params);
             return null;
         }
     }
 
     /** Returns all {@link ScheduledRecording}s. */
-    public static class DvrQueryScheduleFuture
-            extends DvrDbFuture<Void, List<ScheduledRecording>> {
-        public DvrQueryScheduleFuture(Context context) {
-            super(context);
+    public static class DvrQueryScheduleFuture extends DvrDbFuture<Void, List<ScheduledRecording>> {
+        public DvrQueryScheduleFuture(DvrDatabaseHelper dbHelper) {
+            super(dbHelper);
         }
 
         @Override
@@ -127,7 +120,7 @@
                 return null;
             }
             List<ScheduledRecording> scheduledRecordings = new ArrayList<>();
-            try (Cursor c = sDbHelper.query(Schedules.TABLE_NAME, ScheduledRecording.PROJECTION)) {
+            try (Cursor c = mDbHelper.query(Schedules.TABLE_NAME, ScheduledRecording.PROJECTION)) {
                 while (c.moveToNext() && !isCancelled()) {
                     scheduledRecordings.add(ScheduledRecording.fromCursor(c));
                 }
@@ -137,43 +130,40 @@
     }
 
     /** Inserts series recordings. */
-    public static class AddSeriesRecordingFuture
-            extends DvrDbFuture<SeriesRecording, Void> {
-        public AddSeriesRecordingFuture(Context context) {
-            super(context);
+    public static class AddSeriesRecordingFuture extends DvrDbFuture<SeriesRecording, Void> {
+        public AddSeriesRecordingFuture(DvrDatabaseHelper dbHelper) {
+            super(dbHelper);
         }
 
         @Override
         protected final Void dbHelperInBackground(SeriesRecording... params) {
-            sDbHelper.insertSeriesRecordings(params);
+            mDbHelper.insertSeriesRecordings(params);
             return null;
         }
     }
 
     /** Update series recordings. */
-    public static class UpdateSeriesRecordingFuture
-            extends DvrDbFuture<SeriesRecording, Void> {
-        public UpdateSeriesRecordingFuture(Context context) {
-            super(context);
+    public static class UpdateSeriesRecordingFuture extends DvrDbFuture<SeriesRecording, Void> {
+        public UpdateSeriesRecordingFuture(DvrDatabaseHelper dbHelper) {
+            super(dbHelper);
         }
 
         @Override
         protected final Void dbHelperInBackground(SeriesRecording... params) {
-            sDbHelper.updateSeriesRecordings(params);
+            mDbHelper.updateSeriesRecordings(params);
             return null;
         }
     }
 
     /** Delete series recordings. */
-    public static class DeleteSeriesRecordingFuture
-            extends DvrDbFuture<SeriesRecording, Void> {
-        public DeleteSeriesRecordingFuture(Context context) {
-            super(context);
+    public static class DeleteSeriesRecordingFuture extends DvrDbFuture<SeriesRecording, Void> {
+        public DeleteSeriesRecordingFuture(DvrDatabaseHelper dbHelper) {
+            super(dbHelper);
         }
 
         @Override
         protected final Void dbHelperInBackground(SeriesRecording... params) {
-            sDbHelper.deleteSeriesRecordings(params);
+            mDbHelper.deleteSeriesRecordings(params);
             return null;
         }
     }
@@ -183,8 +173,8 @@
             extends DvrDbFuture<Void, List<SeriesRecording>> {
         private static final String TAG = "DvrQuerySeriesRecording";
 
-        public DvrQuerySeriesRecordingFuture(Context context) {
-            super(context);
+        public DvrQuerySeriesRecordingFuture(DvrDatabaseHelper dbHelper) {
+            super(dbHelper);
         }
 
         @Override
@@ -195,7 +185,7 @@
             }
             List<SeriesRecording> scheduledRecordings = new ArrayList<>();
             try (Cursor c =
-                    sDbHelper.query(SeriesRecordings.TABLE_NAME, SeriesRecording.PROJECTION)) {
+                    mDbHelper.query(SeriesRecordings.TABLE_NAME, SeriesRecording.PROJECTION)) {
                 while (c.moveToNext() && !isCancelled()) {
                     scheduledRecordings.add(SeriesRecording.fromCursor(c));
                 }
diff --git a/src/com/android/tv/dvr/provider/DvrDbSync.java b/src/com/android/tv/dvr/provider/DvrDbSync.java
index 7658ca4..c2eae77 100644
--- a/src/com/android/tv/dvr/provider/DvrDbSync.java
+++ b/src/com/android/tv/dvr/provider/DvrDbSync.java
@@ -29,17 +29,19 @@
 import android.support.annotation.MainThread;
 import android.support.annotation.VisibleForTesting;
 import android.util.Log;
+
 import com.android.tv.TvSingletons;
 import com.android.tv.data.ChannelDataManager;
-import com.android.tv.data.Program;
+import com.android.tv.data.api.Program;
 import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
-import com.android.tv.dvr.DvrDataManagerImpl;
 import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.WritableDvrDataManager;
 import com.android.tv.dvr.data.ScheduledRecording;
 import com.android.tv.dvr.data.SeriesRecording;
 import com.android.tv.dvr.recorder.SeriesRecordingScheduler;
 import com.android.tv.util.AsyncDbTask.AsyncQueryProgramTask;
 import com.android.tv.util.TvUriMatcher;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
@@ -49,6 +51,7 @@
 import java.util.Queue;
 import java.util.Set;
 import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
 
 /**
  * A class to synchronizes DVR DB with TvProvider.
@@ -65,9 +68,11 @@
     private static final String TAG = "DvrDbSync";
     private static final boolean DEBUG = false;
 
+    private static final long RECORD_MARGIN_MS = TimeUnit.SECONDS.toMillis(10);
+
     private final Context mContext;
     private final DvrManager mDvrManager;
-    private final DvrDataManagerImpl mDataManager;
+    private final WritableDvrDataManager mDataManager;
     private final ChannelDataManager mChannelDataManager;
     private final Executor mDbExecutor;
     private final Queue<Long> mProgramIdQueue = new LinkedList<>();
@@ -138,7 +143,7 @@
                 }
             };
 
-    public DvrDbSync(Context context, DvrDataManagerImpl dataManager) {
+    public DvrDbSync(Context context, WritableDvrDataManager dataManager) {
         this(
                 context,
                 dataManager,
@@ -151,7 +156,7 @@
     @VisibleForTesting
     DvrDbSync(
             Context context,
-            DvrDataManagerImpl dataManager,
+            WritableDvrDataManager dataManager,
             ChannelDataManager channelDataManager,
             DvrManager dvrManager,
             SeriesRecordingScheduler seriesRecordingScheduler,
@@ -325,10 +330,15 @@
                     // Old program belongs to a series but the new one doesn't.
                     seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule);
                 }
-                // Change start time only when the recording is not started yet.
+                // Change start time only when the recording is not started yet and if it is not
+                // within marginal time of current time. Marginal check is needed to prevent the
+                // update of start time if recording is just triggered or about to get triggered.
+                boolean marginalToCurrentTime = RECORD_MARGIN_MS >
+                        Math.abs(System.currentTimeMillis() - schedule.getStartTimeMs());
                 boolean needToChangeStartTime =
                         schedule.getState() != ScheduledRecording.STATE_RECORDING_IN_PROGRESS
-                                && program.getStartTimeUtcMillis() != schedule.getStartTimeMs();
+                                && program.getStartTimeUtcMillis() != schedule.getStartTimeMs()
+                                && !marginalToCurrentTime;
                 if (needToChangeStartTime) {
                     builder.setStartTimeMs(program.getStartTimeUtcMillis());
                     needUpdate = true;
diff --git a/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java b/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java
index 02e197f..a66e5b0 100644
--- a/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java
+++ b/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java
@@ -25,16 +25,19 @@
 import android.os.Build;
 import android.support.annotation.Nullable;
 import android.support.annotation.WorkerThread;
+
 import com.android.tv.TvSingletons;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.util.PermissionUtils;
-import com.android.tv.data.Program;
+import com.android.tv.data.ProgramImpl;
+import com.android.tv.data.api.Program;
 import com.android.tv.dvr.DvrDataManager;
 import com.android.tv.dvr.data.ScheduledRecording;
 import com.android.tv.dvr.data.SeasonEpisodeNumber;
 import com.android.tv.dvr.data.SeriesRecording;
 import com.android.tv.util.AsyncDbTask.AsyncProgramQueryTask;
 import com.android.tv.util.AsyncDbTask.CursorFilter;
+
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -47,11 +50,11 @@
 public abstract class EpisodicProgramLoadTask {
     private static final String TAG = "EpisodicProgramLoadTask";
 
-    private static final int PROGRAM_ID_INDEX = Program.getColumnIndex(Programs._ID);
+    private static final int PROGRAM_ID_INDEX = ProgramImpl.getColumnIndex(Programs._ID);
     private static final int START_TIME_INDEX =
-            Program.getColumnIndex(Programs.COLUMN_START_TIME_UTC_MILLIS);
+            ProgramImpl.getColumnIndex(Programs.COLUMN_START_TIME_UTC_MILLIS);
     private static final int RECORDING_PROHIBITED_INDEX =
-            Program.getColumnIndex(Programs.COLUMN_RECORDING_PROHIBITED);
+            ProgramImpl.getColumnIndex(Programs.COLUMN_RECORDING_PROHIBITED);
 
     private static final String PARAM_START_TIME = "start_time";
     private static final String PARAM_END_TIME = "end_time";
@@ -289,7 +292,7 @@
                     && mDisallowedProgramIds.contains(c.getLong(PROGRAM_ID_INDEX))) {
                 return false;
             }
-            Program program = Program.fromCursor(c);
+            Program program = ProgramImpl.fromCursor(c);
             for (SeriesRecording seriesRecording : mSeriesRecordings) {
                 boolean programMatches;
                 if (mIgnoreChannelOption) {
diff --git a/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java b/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java
index 696038c..92b4e06 100644
--- a/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java
+++ b/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java
@@ -27,11 +27,12 @@
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.LongSparseArray;
+
 import com.android.tv.TvSingletons;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.util.CollectionUtils;
 import com.android.tv.common.util.SharedPreferencesUtils;
-import com.android.tv.data.Program;
+import com.android.tv.data.api.Program;
 import com.android.tv.data.epg.EpgReader;
 import com.android.tv.dvr.DvrDataManager;
 import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
@@ -43,6 +44,9 @@
 import com.android.tv.dvr.data.SeriesInfo;
 import com.android.tv.dvr.data.SeriesRecording;
 import com.android.tv.dvr.provider.EpisodicProgramLoadTask;
+
+import dagger.Lazy;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -54,7 +58,6 @@
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
-import javax.inject.Provider;
 
 /**
  * Creates the {@link com.android.tv.dvr.data.ScheduledRecording}s for the {@link
@@ -529,10 +532,9 @@
 
     private class FetchSeriesInfoTask extends AsyncTask<Void, Void, SeriesInfo> {
         private final SeriesRecording mSeriesRecording;
-        private final Provider<EpgReader> mEpgReaderProvider;
+        private final Lazy<EpgReader> mEpgReaderProvider;
 
-        FetchSeriesInfoTask(
-                SeriesRecording seriesRecording, Provider<EpgReader> epgReaderProvider) {
+        FetchSeriesInfoTask(SeriesRecording seriesRecording, Lazy<EpgReader> epgReaderProvider) {
             mSeriesRecording = seriesRecording;
             mEpgReaderProvider = epgReaderProvider;
         }
diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
index 5e3caa9..1f4faf3 100644
--- a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
@@ -22,13 +22,16 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
+
+import androidx.leanback.widget.GuidanceStylist.Guidance;
+import androidx.leanback.widget.GuidedAction;
+
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
-import com.android.tv.data.Program;
+import com.android.tv.data.api.Program;
 import com.android.tv.dvr.DvrManager;
 import com.android.tv.dvr.data.RecordedProgram;
+
 import java.util.List;
 
 /**
diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java
index a6bbe13..56ffc88 100644
--- a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java
@@ -22,14 +22,17 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
 import android.text.format.DateUtils;
+
+import androidx.leanback.widget.GuidanceStylist.Guidance;
+import androidx.leanback.widget.GuidedAction;
+
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
-import com.android.tv.data.Program;
+import com.android.tv.data.api.Program;
 import com.android.tv.dvr.DvrManager;
 import com.android.tv.dvr.data.ScheduledRecording;
+
 import java.util.List;
 
 /**
diff --git a/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java b/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java
index 6be35cb..b24281a 100644
--- a/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java
@@ -18,9 +18,9 @@
 
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
-import android.support.v17.leanback.app.GuidedStepFragment;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
+import androidx.leanback.app.GuidedStepFragment;
+import androidx.leanback.widget.GuidanceStylist.Guidance;
+import androidx.leanback.widget.GuidedAction;
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
 import com.android.tv.common.SoftPreconditions;
diff --git a/src/com/android/tv/dvr/ui/DvrConflictFragment.java b/src/com/android/tv/dvr/ui/DvrConflictFragment.java
index 649cc89..5e0a96b 100644
--- a/src/com/android/tv/dvr/ui/DvrConflictFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrConflictFragment.java
@@ -21,22 +21,25 @@
 import android.os.Bundle;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+
+import androidx.leanback.widget.GuidanceStylist.Guidance;
+import androidx.leanback.widget.GuidedAction;
+
 import com.android.tv.MainActivity;
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
 import com.android.tv.common.SoftPreconditions;
-import com.android.tv.data.Program;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 import com.android.tv.dvr.data.ScheduledRecording;
 import com.android.tv.dvr.recorder.ConflictChecker;
 import com.android.tv.dvr.recorder.ConflictChecker.OnUpcomingConflictChangeListener;
 import com.android.tv.util.Utils;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
diff --git a/src/com/android/tv/dvr/ui/DvrGuidedActionsStylist.java b/src/com/android/tv/dvr/ui/DvrGuidedActionsStylist.java
index 611962d..81fc3ed 100644
--- a/src/com/android/tv/dvr/ui/DvrGuidedActionsStylist.java
+++ b/src/com/android/tv/dvr/ui/DvrGuidedActionsStylist.java
@@ -17,8 +17,8 @@
 package com.android.tv.dvr.ui;
 
 import android.content.Context;
-import android.support.v17.leanback.app.GuidedStepFragment;
-import android.support.v17.leanback.widget.GuidedActionsStylist;
+import androidx.leanback.app.GuidedStepFragment;
+import androidx.leanback.widget.GuidedActionsStylist;
 import android.util.TypedValue;
 import android.view.LayoutInflater;
 import android.view.View;
diff --git a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java
index a900cc7..fda4cde 100644
--- a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java
@@ -20,9 +20,9 @@
 import android.app.DialogFragment;
 import android.content.Context;
 import android.os.Bundle;
-import android.support.v17.leanback.widget.GuidanceStylist;
-import android.support.v17.leanback.widget.GuidedAction;
-import android.support.v17.leanback.widget.VerticalGridView;
+import androidx.leanback.widget.GuidanceStylist;
+import androidx.leanback.widget.GuidedAction;
+import androidx.leanback.widget.VerticalGridView;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
diff --git a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java
index e6b54f6..e8f501e 100644
--- a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java
@@ -20,13 +20,15 @@
 import android.content.Context;
 import android.content.DialogInterface;
 import android.os.Bundle;
-import android.support.v17.leanback.app.GuidedStepFragment;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 
+import androidx.leanback.app.GuidedStepFragment;
+
 import com.android.tv.MainActivity;
 import com.android.tv.R;
+import com.android.tv.data.ProgramImpl;
 import com.android.tv.dialog.HalfSizedDialogFragment;
 import com.android.tv.dvr.ui.DvrConflictFragment.DvrChannelWatchConflictFragment;
 import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment;
@@ -36,7 +38,7 @@
 public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment {
     /** Key for input ID. Type: String. */
     public static final String KEY_INPUT_ID = "DvrHalfSizedDialogFragment.input_id";
-    /** Key for the program. Type: {@link com.android.tv.data.Program}. */
+    /** Key for the program. Type: {@link ProgramImpl}. */
     public static final String KEY_PROGRAM = "DvrHalfSizedDialogFragment.program";
     /** Key for the channel ID. Type: long. */
     public static final String KEY_CHANNEL_ID = "DvrHalfSizedDialogFragment.channel_id";
diff --git a/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java b/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java
index 6fba4d9..01631b9 100644
--- a/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java
@@ -20,8 +20,8 @@
 import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
+import androidx.leanback.widget.GuidanceStylist.Guidance;
+import androidx.leanback.widget.GuidedAction;
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
 import com.android.tv.common.SoftPreconditions;
diff --git a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java
index 02b2da1..3237acd 100644
--- a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java
@@ -21,8 +21,8 @@
 import android.content.Intent;
 import android.os.Bundle;
 import android.provider.Settings;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
+import androidx.leanback.widget.GuidanceStylist.Guidance;
+import androidx.leanback.widget.GuidedAction;
 import android.util.Log;
 import com.android.tv.R;
 import com.android.tv.ui.DetailsActivity;
diff --git a/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java b/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java
index 5bb97e9..ae41f50 100644
--- a/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java
@@ -22,9 +22,9 @@
 import android.graphics.Typeface;
 import android.os.Build;
 import android.os.Bundle;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
-import android.support.v17.leanback.widget.GuidedActionsStylist;
+import androidx.leanback.widget.GuidanceStylist.Guidance;
+import androidx.leanback.widget.GuidedAction;
+import androidx.leanback.widget.GuidedActionsStylist;
 import android.view.View;
 import android.widget.ImageView;
 import android.widget.TextView;
diff --git a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
index 72603d0..7131f62 100644
--- a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
@@ -22,18 +22,21 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
-import android.support.v17.leanback.app.GuidedStepFragment;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
 import android.text.format.DateUtils;
+
+import androidx.leanback.app.GuidedStepFragment;
+import androidx.leanback.widget.GuidanceStylist.Guidance;
+import androidx.leanback.widget.GuidedAction;
+
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
 import com.android.tv.common.SoftPreconditions;
-import com.android.tv.data.Program;
+import com.android.tv.data.ProgramImpl;
 import com.android.tv.dvr.DvrManager;
 import com.android.tv.dvr.data.ScheduledRecording;
 import com.android.tv.dvr.data.SeriesRecording;
 import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment;
+
 import java.util.Collections;
 import java.util.List;
 
@@ -52,7 +55,7 @@
     private static final int ACTION_RECORD_EPISODE = 1;
     private static final int ACTION_RECORD_SERIES = 2;
 
-    private Program mProgram;
+    private ProgramImpl mProgram;
     private boolean mAddCurrentProgramToSeries;
 
     @Override
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
index a237f1d..730237c 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
@@ -20,15 +20,13 @@
 import android.content.pm.PackageManager;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
-import android.support.v17.leanback.app.GuidedStepFragment;
+import androidx.leanback.app.GuidedStepFragment;
 import android.util.Log;
 import android.widget.Toast;
-
 import com.android.tv.R;
 import com.android.tv.Starter;
 import com.android.tv.TvSingletons;
 import com.android.tv.dvr.DvrManager;
-
 import java.util.ArrayList;
 import java.util.List;
 
@@ -70,7 +68,7 @@
                         && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                     deleteSelectedIds(true);
                 } else {
-                    // NOTE: If Live TV ever supports both embedded and separate DVR inputs
+                    // NOTE: If TV app ever supports both embedded and separate DVR inputs
                     // then we should try to do the delete regardless.
                     Log.i(
                             TAG,
@@ -93,14 +91,14 @@
             dvrManager.removeRecordedPrograms(mIdsToDelete, deleteFiles);
         }
         Toast.makeText(
-                this,
-                getResources()
-                        .getQuantityString(
-                                R.plurals.dvr_msg_episodes_deleted,
-                                mIdsToDelete.size(),
-                                mIdsToDelete.size(),
-                                recordingSize),
-                Toast.LENGTH_LONG)
+                        this,
+                        getResources()
+                                .getQuantityString(
+                                        R.plurals.dvr_msg_episodes_deleted,
+                                        mIdsToDelete.size(),
+                                        mIdsToDelete.size(),
+                                        recordingSize),
+                        Toast.LENGTH_LONG)
                 .show();
         finish();
     }
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java
index ff21323..10ef226 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java
@@ -19,10 +19,10 @@
 import android.content.Context;
 import android.media.tv.TvInputManager;
 import android.os.Bundle;
-import android.support.v17.leanback.app.GuidedStepFragment;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
-import android.support.v17.leanback.widget.GuidedActionsStylist;
+import androidx.leanback.app.GuidedStepFragment;
+import androidx.leanback.widget.GuidanceStylist.Guidance;
+import androidx.leanback.widget.GuidedAction;
+import androidx.leanback.widget.GuidedActionsStylist;
 import android.text.TextUtils;
 import android.view.ViewGroup.LayoutParams;
 import android.widget.Toast;
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesScheduledDialogActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesScheduledDialogActivity.java
index 9acb5b5..d72099b 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesScheduledDialogActivity.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesScheduledDialogActivity.java
@@ -18,7 +18,7 @@
 
 import android.app.Activity;
 import android.os.Bundle;
-import android.support.v17.leanback.app.GuidedStepFragment;
+import androidx.leanback.app.GuidedStepFragment;
 import com.android.tv.R;
 
 public class DvrSeriesScheduledDialogActivity extends Activity {
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java
index c6e2685..7d36990 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java
@@ -20,22 +20,26 @@
 import android.content.Intent;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
-import android.support.v17.leanback.widget.GuidanceStylist;
-import android.support.v17.leanback.widget.GuidedAction;
+
+import androidx.leanback.widget.GuidanceStylist;
+import androidx.leanback.widget.GuidedAction;
+
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
-import com.android.tv.data.Program;
+import com.android.tv.data.ProgramImpl;
+import com.android.tv.data.api.Program;
 import com.android.tv.dvr.DvrScheduleManager;
 import com.android.tv.dvr.data.ScheduledRecording;
 import com.android.tv.dvr.data.SeriesRecording;
 import com.android.tv.dvr.ui.list.DvrSchedulesActivity;
 import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment;
+
 import java.util.List;
 
 public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment {
     /**
      * The key for program list which will be passed to {@link DvrSeriesSchedulesFragment}. Type:
-     * List<{@link Program}>
+     * List<{@link ProgramImpl}>
      */
     public static final String SERIES_SCHEDULED_KEY_PROGRAMS = "series_scheduled_key_programs";
 
@@ -46,6 +50,7 @@
     private SeriesRecording mSeriesRecording;
     private boolean mShowViewScheduleOption;
     private List<Program> mPrograms;
+    private String mSeriesRecordingTitle;
 
     private int mSchedulesAddedCount = 0;
     private boolean mHasConflict = false;
@@ -75,6 +80,7 @@
             getActivity().finish();
             return;
         }
+        mSeriesRecordingTitle = mSeriesRecording.getTitle();
         mPrograms = (List<Program>) BigArguments.getArgument(SERIES_SCHEDULED_KEY_PROGRAMS);
         BigArguments.reset();
         mSchedulesAddedCount =
@@ -162,7 +168,7 @@
                             R.plurals.dvr_series_scheduled_no_conflict,
                             mSchedulesAddedCount,
                             mSchedulesAddedCount,
-                            mSeriesRecording.getTitle());
+                            mSeriesRecordingTitle);
         } else {
             // mInThisSeriesConflictCount equals 0 and mOutThisSeriesConflictCount equals 0 means
             // mHasConflict is false. So we don't need to check that case.
@@ -172,7 +178,7 @@
                                 R.plurals.dvr_series_scheduled_this_and_other_series_conflict,
                                 mSchedulesAddedCount,
                                 mSchedulesAddedCount,
-                                mSeriesRecording.getTitle(),
+                                mSeriesRecordingTitle,
                                 mInThisSeriesConflictCount + mOutThisSeriesConflictCount);
             } else if (mInThisSeriesConflictCount != 0) {
                 return getResources()
@@ -180,7 +186,7 @@
                                 R.plurals.dvr_series_recording_scheduled_only_this_series_conflict,
                                 mSchedulesAddedCount,
                                 mSchedulesAddedCount,
-                                mSeriesRecording.getTitle(),
+                                mSeriesRecordingTitle,
                                 mInThisSeriesConflictCount);
             } else {
                 if (mOutThisSeriesConflictCount == 1) {
@@ -189,14 +195,14 @@
                                     R.plurals.dvr_series_scheduled_only_other_series_one_conflict,
                                     mSchedulesAddedCount,
                                     mSchedulesAddedCount,
-                                    mSeriesRecording.getTitle());
+                                    mSeriesRecordingTitle);
                 } else {
                     return getResources()
                             .getQuantityString(
                                     R.plurals.dvr_series_scheduled_only_other_series_many_conflicts,
                                     mSchedulesAddedCount,
                                     mSchedulesAddedCount,
-                                    mSeriesRecording.getTitle(),
+                                    mSeriesRecordingTitle,
                                     mOutThisSeriesConflictCount);
                 }
             }
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java
index 1a51cf4..eb3ca28 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java
@@ -19,10 +19,13 @@
 import android.app.Activity;
 import android.graphics.drawable.ColorDrawable;
 import android.os.Bundle;
-import android.support.v17.leanback.app.GuidedStepFragment;
+
+import androidx.leanback.app.GuidedStepFragment;
+
 import com.android.tv.R;
 import com.android.tv.Starter;
 import com.android.tv.common.SoftPreconditions;
+import com.android.tv.data.ProgramImpl;
 
 /** Activity to show details view in DVR. */
 public class DvrSeriesSettingsActivity extends Activity {
@@ -40,7 +43,7 @@
     public static final String IS_WINDOW_TRANSLUCENT = "windows_translucent";
     /**
      * Name of the program list. The list contains the programs which belong to the series. Type:
-     * List<{@link com.android.tv.data.Program}>
+     * List<{@link ProgramImpl}>
      */
     public static final String PROGRAM_LIST = "program_list";
 
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java
index eadb3b9..7fc201c 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java
@@ -21,17 +21,19 @@
 import android.content.Context;
 import android.os.Build;
 import android.os.Bundle;
-import android.support.v17.leanback.app.GuidedStepFragment;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
-import android.support.v17.leanback.widget.GuidedActionsStylist;
 import android.util.LongSparseArray;
+
+import androidx.leanback.app.GuidedStepFragment;
+import androidx.leanback.widget.GuidanceStylist.Guidance;
+import androidx.leanback.widget.GuidedAction;
+import androidx.leanback.widget.GuidedActionsStylist;
+
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
 import com.android.tv.data.ChannelDataManager;
 import com.android.tv.data.ChannelImpl;
-import com.android.tv.data.Program;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 import com.android.tv.dvr.DvrDataManager;
 import com.android.tv.dvr.DvrManager;
 import com.android.tv.dvr.data.ScheduledRecording;
@@ -39,6 +41,7 @@
 import com.android.tv.dvr.data.SeriesRecording;
 import com.android.tv.dvr.data.SeriesRecording.ChannelOption;
 import com.android.tv.dvr.recorder.SeriesRecordingScheduler;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
@@ -69,6 +72,7 @@
     private Program mCurrentProgram;
 
     private String mFragmentTitle;
+    private String mSeriesRecordingTitle;
     private String mProrityActionTitle;
     private String mProrityActionHighestText;
     private String mProrityActionLowestText;
@@ -92,6 +96,7 @@
             getActivity().finish();
             return;
         }
+        mSeriesRecordingTitle = mSeriesRecording.getTitle();
         mShowViewScheduleOptionInDialog =
                 getArguments()
                         .getBoolean(DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG);
@@ -161,9 +166,8 @@
 
     @Override
     public Guidance onCreateGuidance(Bundle savedInstanceState) {
-        String breadcrumb = mSeriesRecording.getTitle();
         String title = mFragmentTitle;
-        return new Guidance(title, null, breadcrumb, null);
+        return new Guidance(title, null, mSeriesRecordingTitle, null);
     }
 
     @Override
diff --git a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
index 1ab4c50..1475a8c 100644
--- a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
@@ -23,13 +23,17 @@
 import android.os.Bundle;
 import android.support.annotation.IntDef;
 import android.support.annotation.NonNull;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
+
+import androidx.leanback.widget.GuidanceStylist.Guidance;
+import androidx.leanback.widget.GuidedAction;
+
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
+import com.android.tv.data.ProgramImpl;
 import com.android.tv.dvr.DvrDataManager;
 import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
 import com.android.tv.dvr.data.ScheduledRecording;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.List;
@@ -44,7 +48,7 @@
 public class DvrStopRecordingFragment extends DvrGuidedStepFragment {
     /** The action ID for the stop action. */
     public static final int ACTION_STOP = 1;
-    /** Key for the program. Type: {@link com.android.tv.data.Program}. */
+    /** Key for the program. Type: {@link ProgramImpl}. */
     public static final String KEY_REASON = "DvrStopRecordingFragment.type";
 
     @Retention(RetentionPolicy.SOURCE)
diff --git a/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingDialogFragment.java b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingDialogFragment.java
index 15abf90..4a8ce04 100644
--- a/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingDialogFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingDialogFragment.java
@@ -18,7 +18,7 @@
 
 import android.app.DialogFragment;
 import android.os.Bundle;
-import android.support.v17.leanback.app.GuidedStepFragment;
+import androidx.leanback.app.GuidedStepFragment;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
diff --git a/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java
index 99211fd..0b8f5df 100644
--- a/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java
@@ -20,8 +20,8 @@
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
-import android.support.v17.leanback.widget.GuidanceStylist;
-import android.support.v17.leanback.widget.GuidedAction;
+import androidx.leanback.widget.GuidanceStylist;
+import androidx.leanback.widget.GuidedAction;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
diff --git a/src/com/android/tv/dvr/ui/DvrUiHelper.java b/src/com/android/tv/dvr/ui/DvrUiHelper.java
index a121cf9..657abfa 100644
--- a/src/com/android/tv/dvr/ui/DvrUiHelper.java
+++ b/src/com/android/tv/dvr/ui/DvrUiHelper.java
@@ -33,6 +33,7 @@
 import android.text.Spannable;
 import android.text.SpannableString;
 import android.text.SpannableStringBuilder;
+import android.text.Spanned;
 import android.text.TextUtils;
 import android.text.style.TextAppearanceSpan;
 import android.widget.ImageView;
@@ -44,9 +45,9 @@
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.recording.RecordingStorageStatusManager;
 import com.android.tv.common.util.CommonUtils;
-import com.android.tv.data.BaseProgram;
-import com.android.tv.data.Program;
+import com.android.tv.data.api.BaseProgram;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 import com.android.tv.dialog.HalfSizedDialogFragment;
 import com.android.tv.dvr.DvrManager;
 import com.android.tv.dvr.data.RecordedProgram;
@@ -75,6 +76,7 @@
 import com.android.tv.util.ToastUtils;
 import com.android.tv.util.Utils;
 
+import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -122,7 +124,7 @@
             return;
         }
         Bundle args = new Bundle();
-        args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program);
+        args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program.toParcelable());
         args.putBoolean(
                 DvrScheduleFragment.KEY_ADD_CURRENT_PROGRAM_TO_SERIES, addCurrentProgramToSeries);
         showDialogFragment(activity, new DvrScheduleDialogFragment(), args, true, true);
@@ -144,7 +146,7 @@
             return;
         }
         Bundle args = new Bundle();
-        args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program);
+        args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program.toParcelable());
         showDialogFragment(activity, new DvrProgramConflictDialogFragment(), args, false, true);
     }
 
@@ -227,7 +229,7 @@
             return;
         }
         Bundle args = new Bundle();
-        args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program);
+        args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program.toParcelable());
         showDialogFragment(activity, new DvrAlreadyScheduledDialogFragment(), args, false, true);
     }
 
@@ -237,14 +239,18 @@
             return;
         }
         Bundle args = new Bundle();
-        args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program);
+        args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program.toParcelable());
         showDialogFragment(activity, new DvrAlreadyRecordedDialogFragment(), args, false, true);
     }
 
     /** Shows program information dialog. */
     public static void showWriteStoragePermissionRationaleDialog(Activity activity) {
-        showDialogFragment(activity, new DvrWriteStoragePermissionRationaleDialogFragment(),
-                new Bundle(), false, false);
+        showDialogFragment(
+                activity,
+                new DvrWriteStoragePermissionRationaleDialogFragment(),
+                new Bundle(),
+                false,
+                false);
     }
 
     /**
@@ -459,7 +465,7 @@
             boolean removeEmptySeriesSchedule,
             boolean isWindowTranslucent,
             boolean showViewScheduleOptionInDialog,
-            Program currentProgram) {
+            @Nullable Program currentProgram) {
         SeriesRecording series =
                 TvSingletons.getSingletons(context)
                         .getDvrDataManager()
@@ -481,13 +487,15 @@
                     new EpisodicProgramLoadTask(context, series) {
                         @Override
                         protected void onPostExecute(List<Program> loadedPrograms) {
-                            sProgressDialog.dismiss();
-                            sProgressDialog = null;
+                            if (sProgressDialog != null) {
+                                sProgressDialog.dismiss();
+                                sProgressDialog = null;
+                            }
                             startSeriesSettingsActivityInternal(
                                     context,
                                     seriesRecordingId,
                                     loadedPrograms == null
-                                            ? Collections.EMPTY_LIST
+                                            ? ImmutableList.of()
                                             : loadedPrograms,
                                     removeEmptySeriesSchedule,
                                     isWindowTranslucent,
@@ -524,7 +532,7 @@
             boolean removeEmptySeriesSchedule,
             boolean isWindowTranslucent,
             boolean showViewScheduleOptionInDialog,
-            Program currentProgram) {
+            @Nullable Program currentProgram) {
         SoftPreconditions.checkState(
                 programs != null, TAG, "Start series settings activity but programs is null");
         Intent intent = new Intent(context, DvrSeriesSettingsActivity.class);
@@ -537,7 +545,9 @@
         intent.putExtra(
                 DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG,
                 showViewScheduleOptionInDialog);
-        intent.putExtra(DvrSeriesSettingsActivity.CURRENT_PROGRAM, currentProgram);
+        if (currentProgram != null) {
+            intent.putExtra(DvrSeriesSettingsActivity.CURRENT_PROGRAM, currentProgram.toParcelable());
+        }
         context.startActivity(intent);
     }
 
@@ -682,16 +692,18 @@
         }
         SpannableStringBuilder builder;
         if (TextUtils.isEmpty(seasonNumber) || seasonNumber.equals("0")) {
-            builder =
+            Spanned temp =
                     TextUtils.isEmpty(episodeNumber)
-                            ? new SpannableStringBuilder(title)
-                            : new SpannableStringBuilder(Html.fromHtml(context.getString(
-                                    R.string.program_title_with_episode_number_no_season,
-                                    title,
-                                    episodeNumber)));
+                            ? SpannableStringBuilder.valueOf(title)
+                            : Html.fromHtml(
+                                    context.getString(
+                                            R.string.program_title_with_episode_number_no_season,
+                                            title,
+                                            episodeNumber));
+            builder = SpannableStringBuilder.valueOf(temp);
         } else {
             builder =
-                    new SpannableStringBuilder(
+                    SpannableStringBuilder.valueOf(
                             Html.fromHtml(
                                     context.getString(
                                             R.string.program_title_with_episode_number,
diff --git a/src/com/android/tv/dvr/ui/DvrWriteStoragePermissionRationaleFragment.java b/src/com/android/tv/dvr/ui/DvrWriteStoragePermissionRationaleFragment.java
index c93f583..25f7f38 100644
--- a/src/com/android/tv/dvr/ui/DvrWriteStoragePermissionRationaleFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrWriteStoragePermissionRationaleFragment.java
@@ -19,8 +19,8 @@
 import android.app.Activity;
 import android.content.res.Resources;
 import android.os.Bundle;
-import android.support.v17.leanback.widget.GuidanceStylist;
-import android.support.v17.leanback.widget.GuidedAction;
+import androidx.leanback.widget.GuidanceStylist;
+import androidx.leanback.widget.GuidedAction;
 
 import com.android.tv.R;
 
diff --git a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java
index 1eb8080..7a26d5e 100644
--- a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java
+++ b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java
@@ -17,8 +17,8 @@
 package com.android.tv.dvr.ui;
 
 import android.support.annotation.VisibleForTesting;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.PresenterSelector;
+import androidx.leanback.widget.ArrayObjectAdapter;
+import androidx.leanback.widget.PresenterSelector;
 import com.android.tv.common.SoftPreconditions;
 import java.util.ArrayList;
 import java.util.Collection;
diff --git a/src/com/android/tv/dvr/ui/TrackedGuidedStepFragment.java b/src/com/android/tv/dvr/ui/TrackedGuidedStepFragment.java
index 0172f76..c0a57c0 100644
--- a/src/com/android/tv/dvr/ui/TrackedGuidedStepFragment.java
+++ b/src/com/android/tv/dvr/ui/TrackedGuidedStepFragment.java
@@ -17,8 +17,8 @@
 package com.android.tv.dvr.ui;
 
 import android.content.Context;
-import android.support.v17.leanback.app.GuidedStepFragment;
-import android.support.v17.leanback.widget.GuidedAction;
+import androidx.leanback.app.GuidedStepFragment;
+import androidx.leanback.widget.GuidedAction;
 import com.android.tv.TvSingletons;
 import com.android.tv.analytics.Tracker;
 
diff --git a/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java
index 41ace9a..a06705c 100644
--- a/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java
+++ b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java
@@ -17,10 +17,10 @@
 package com.android.tv.dvr.ui.browse;
 
 import android.graphics.drawable.Drawable;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.widget.Action;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.PresenterSelector;
+import androidx.leanback.R;
+import androidx.leanback.widget.Action;
+import androidx.leanback.widget.Presenter;
+import androidx.leanback.widget.PresenterSelector;
 import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.View;
diff --git a/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java
index 8c311d6..6d2402f 100644
--- a/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java
@@ -19,12 +19,13 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.media.tv.TvInputManager;
-import android.support.v17.leanback.widget.Action;
-import android.support.v17.leanback.widget.OnActionClickedListener;
-import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+
+import androidx.leanback.widget.Action;
+import androidx.leanback.widget.OnActionClickedListener;
+import androidx.leanback.widget.SparseArrayObjectAdapter;
+
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
-import com.android.tv.common.flags.has.HasConcurrentDvrPlaybackFlags;
 import com.android.tv.dialog.HalfSizedDialogFragment;
 import com.android.tv.dvr.DvrDataManager;
 import com.android.tv.dvr.DvrManager;
@@ -33,8 +34,13 @@
 import com.android.tv.dvr.data.ScheduledRecording;
 import com.android.tv.dvr.ui.DvrStopRecordingFragment;
 import com.android.tv.dvr.ui.DvrUiHelper;
+
+import dagger.android.AndroidInjection;
+
 import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
 
+import javax.inject.Inject;
+
 /** {@link RecordingDetailsFragment} for current recording in DVR. */
 public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment {
     private static final int ACTION_STOP_RECORDING = 1;
@@ -43,8 +49,8 @@
 
     private DvrDataManager mDvrDataManger;
     private RecordedProgram mRecordedProgram;
-    private DvrWatchedPositionManager mDvrWatchedPositionManager;
-    private ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
+    @Inject DvrWatchedPositionManager mDvrWatchedPositionManager;
+    @Inject ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
     private boolean mPaused;
     private final DvrDataManager.ScheduledRecordingListener mScheduledRecordingListener =
             new DvrDataManager.ScheduledRecordingListener() {
@@ -76,12 +82,10 @@
 
     @Override
     public void onAttach(Context context) {
+        AndroidInjection.inject(this);
         super.onAttach(context);
         mDvrDataManger = TvSingletons.getSingletons(context).getDvrDataManager();
         mDvrDataManger.addScheduledRecordingListener(mScheduledRecordingListener);
-        mDvrWatchedPositionManager =
-                TvSingletons.getSingletons(getActivity()).getDvrWatchedPositionManager();
-        mConcurrentDvrPlaybackFlags = HasConcurrentDvrPlaybackFlags.fromContext(context);
     }
 
     @Override
@@ -115,9 +119,7 @@
                         res.getString(R.string.dvr_detail_stop_recording),
                         null,
                         res.getDrawable(R.drawable.lb_ic_stop)));
-        if (mConcurrentDvrPlaybackFlags.enabled()
-                && mRecordedProgram != null
-                && mRecordedProgram.isPartial()) {
+        if (mRecordedProgram != null && mRecordedProgram.isPartial()) {
             if (mDvrWatchedPositionManager.getWatchedStatus(mRecordedProgram)
                     == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) {
                 adapter.set(
diff --git a/src/com/android/tv/dvr/ui/browse/DetailsContent.java b/src/com/android/tv/dvr/ui/browse/DetailsContent.java
index e179743..b5e0526 100644
--- a/src/com/android/tv/dvr/ui/browse/DetailsContent.java
+++ b/src/com/android/tv/dvr/ui/browse/DetailsContent.java
@@ -20,10 +20,11 @@
 import android.media.tv.TvContract;
 import android.support.annotation.Nullable;
 import android.text.TextUtils;
+
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
-import com.android.tv.data.Program;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 import com.android.tv.dvr.data.RecordedProgram;
 import com.android.tv.dvr.data.ScheduledRecording;
 import com.android.tv.dvr.data.SeriesRecording;
@@ -126,9 +127,10 @@
     }
 
     private static String getErrorMessage(Context context, ScheduledRecording recording) {
-        int reason = recording.getFailedReason() == null
-                ? ScheduledRecording.FAILED_REASON_OTHER
-                : recording.getFailedReason();
+        int reason =
+                recording.getFailedReason() == null
+                        ? ScheduledRecording.FAILED_REASON_OTHER
+                        : recording.getFailedReason();
         switch (reason) {
             case ScheduledRecording.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED:
                 return context.getString(R.string.dvr_recording_failed_not_started);
@@ -136,8 +138,7 @@
                 return context.getString(R.string.dvr_recording_failed_resource_busy);
             case ScheduledRecording.FAILED_REASON_INPUT_UNAVAILABLE:
                 return context.getString(
-                        R.string.dvr_recording_failed_input_unavailable,
-                        recording.getInputId());
+                        R.string.dvr_recording_failed_input_unavailable, recording.getInputId());
             case ScheduledRecording.FAILED_REASON_INPUT_DVR_UNSUPPORTED:
                 return context.getString(R.string.dvr_recording_failed_input_dvr_unsupported);
             case ScheduledRecording.FAILED_REASON_INSUFFICIENT_SPACE:
diff --git a/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java
index 6b5fd1f..fafc70c 100644
--- a/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java
+++ b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java
@@ -24,7 +24,7 @@
 import android.content.Context;
 import android.graphics.Paint;
 import android.graphics.Paint.FontMetricsInt;
-import android.support.v17.leanback.widget.Presenter;
+import androidx.leanback.widget.Presenter;
 import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -40,10 +40,10 @@
 /**
  * An {@link Presenter} for rendering a detailed description of an DVR item. Typically this
  * Presenter will be used in a {@link
- * android.support.v17.leanback.widget.DetailsOverviewRowPresenter}. Most codes of this class is
- * originated from {@link android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter}.
+ * androidx.leanback.widget.DetailsOverviewRowPresenter}. Most codes of this class is
+ * originated from {@link androidx.leanback.widget.AbstractDetailsDescriptionPresenter}.
  * The latter class are re-used to provide a customized version of {@link
- * android.support.v17.leanback.widget.DetailsOverviewRow}.
+ * androidx.leanback.widget.DetailsOverviewRow}.
  */
 public class DetailsContentPresenter extends Presenter {
     /** The ViewHolder for the {@link DetailsContentPresenter}. */
diff --git a/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java
index 4e41dae..8a4c785 100644
--- a/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java
+++ b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java
@@ -21,7 +21,7 @@
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.os.Handler;
-import android.support.v17.leanback.app.BackgroundManager;
+import androidx.leanback.app.BackgroundManager;
 
 /** The Background Helper. */
 public class DetailsViewBackgroundHelper {
diff --git a/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java
index 5743ea5..7262b4a 100644
--- a/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java
@@ -22,13 +22,13 @@
 import android.os.Bundle;
 import com.android.tv.R;
 import com.android.tv.Starter;
-import com.android.tv.perf.PerformanceMonitorManagerFactory;
+import com.android.tv.perf.StartupMeasureFactory;
 
 /** {@link android.app.Activity} for DVR UI. */
 public class DvrBrowseActivity extends Activity {
 
     {
-        PerformanceMonitorManagerFactory.create().getStartupMeasure().onActivityInit();
+        StartupMeasureFactory.create().onActivityInit();
     }
 
     private DvrBrowseFragment mFragment;
diff --git a/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java
index 17ba193..786942e 100644
--- a/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java
@@ -21,13 +21,14 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
-import android.support.v17.leanback.app.BrowseFragment;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.ClassPresenterSelector;
-import android.support.v17.leanback.widget.HeaderItem;
-import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.TitleViewAdapter;
+import android.text.TextUtils;
+import androidx.leanback.app.BrowseFragment;
+import androidx.leanback.widget.ArrayObjectAdapter;
+import androidx.leanback.widget.ClassPresenterSelector;
+import androidx.leanback.widget.HeaderItem;
+import androidx.leanback.widget.ListRow;
+import androidx.leanback.widget.Presenter;
+import androidx.leanback.widget.TitleViewAdapter;
 import android.util.Log;
 import android.view.View;
 import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
@@ -475,7 +476,7 @@
         mRecentAdapter.add(recordedProgram);
         String seriesId = recordedProgram.getSeriesId();
         SeriesRecording seriesRecording = null;
-        if (seriesId != null) {
+        if (!TextUtils.isEmpty(seriesId)) {
             seriesRecording = mDvrDataManager.getSeriesRecording(seriesId);
             RecordedProgram latestProgram = mSeriesId2LatestProgram.get(seriesId);
             if (latestProgram == null
@@ -499,7 +500,7 @@
     private void handleRecordedProgramRemoved(RecordedProgram recordedProgram) {
         mRecentAdapter.remove(recordedProgram);
         String seriesId = recordedProgram.getSeriesId();
-        if (seriesId != null) {
+        if (!TextUtils.isEmpty(seriesId)) {
             SeriesRecording seriesRecording = mDvrDataManager.getSeriesRecording(seriesId);
             RecordedProgram latestProgram =
                     mSeriesId2LatestProgram.get(recordedProgram.getSeriesId());
@@ -520,7 +521,7 @@
         mRecentAdapter.change(recordedProgram);
         String seriesId = recordedProgram.getSeriesId();
         SeriesRecording seriesRecording = null;
-        if (seriesId != null) {
+        if (!TextUtils.isEmpty(seriesId)) {
             seriesRecording = mDvrDataManager.getSeriesRecording(seriesId);
             RecordedProgram latestProgram = mSeriesId2LatestProgram.get(seriesId);
             if (latestProgram == null
@@ -663,7 +664,7 @@
             }
         }
         if (getSelectedPosition() >= mRowsAdapter.size()) {
-            setSelectedPosition(mRecentAdapter.size() - 1);
+            setSelectedPosition(mRowsAdapter.size() - 1);
         }
     }
 
diff --git a/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java
index f90981f..a38180a 100644
--- a/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java
@@ -25,15 +25,15 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.support.annotation.Nullable;
-import android.support.v17.leanback.app.DetailsFragment;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.ClassPresenterSelector;
-import android.support.v17.leanback.widget.DetailsOverviewRow;
-import android.support.v17.leanback.widget.DetailsOverviewRowPresenter;
-import android.support.v17.leanback.widget.OnActionClickedListener;
-import android.support.v17.leanback.widget.PresenterSelector;
-import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
-import android.support.v17.leanback.widget.VerticalGridView;
+import androidx.leanback.app.DetailsFragment;
+import androidx.leanback.widget.ArrayObjectAdapter;
+import androidx.leanback.widget.ClassPresenterSelector;
+import androidx.leanback.widget.DetailsOverviewRow;
+import androidx.leanback.widget.DetailsOverviewRowPresenter;
+import androidx.leanback.widget.OnActionClickedListener;
+import androidx.leanback.widget.PresenterSelector;
+import androidx.leanback.widget.SparseArrayObjectAdapter;
+import androidx.leanback.widget.VerticalGridView;
 import android.text.TextUtils;
 import android.widget.Toast;
 import com.android.tv.R;
diff --git a/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java b/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java
index 4298d86..ebdee32 100644
--- a/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java
@@ -19,7 +19,7 @@
 import android.app.Activity;
 import android.content.Context;
 import android.support.annotation.CallSuper;
-import android.support.v17.leanback.widget.Presenter;
+import androidx.leanback.widget.Presenter;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.ViewGroup;
diff --git a/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java b/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java
index a2d1cb2..625f8f7 100644
--- a/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java
@@ -17,7 +17,7 @@
 package com.android.tv.dvr.ui.browse;
 
 import android.content.Context;
-import android.support.v17.leanback.widget.ListRowPresenter;
+import androidx.leanback.widget.ListRowPresenter;
 import android.view.ViewGroup;
 import com.android.tv.R;
 
diff --git a/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java
index bf96354..5f58af8 100644
--- a/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java
@@ -19,9 +19,9 @@
 import android.content.res.Resources;
 import android.media.tv.TvInputManager;
 import android.os.Bundle;
-import android.support.v17.leanback.widget.Action;
-import android.support.v17.leanback.widget.OnActionClickedListener;
-import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import androidx.leanback.widget.Action;
+import androidx.leanback.widget.OnActionClickedListener;
+import androidx.leanback.widget.SparseArrayObjectAdapter;
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
 import com.android.tv.common.util.PermissionUtils;
@@ -32,7 +32,7 @@
 import com.android.tv.dvr.ui.DvrUiHelper;
 import com.android.tv.ui.DetailsActivity;
 
-/** {@link android.support.v17.leanback.app.DetailsFragment} for recorded program in DVR. */
+/** {@link androidx.leanback.app.DetailsFragment} for recorded program in DVR. */
 public class RecordedProgramDetailsFragment extends DvrDetailsFragment
         implements DvrDataManager.RecordedProgramListener {
     private static final int ACTION_RESUME_PLAYING = 1;
diff --git a/src/com/android/tv/dvr/ui/browse/RecordingCardView.java b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java
index c83ceaf..ac7c574 100644
--- a/src/com/android/tv/dvr/ui/browse/RecordingCardView.java
+++ b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java
@@ -23,14 +23,16 @@
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.support.annotation.Nullable;
-import android.support.v17.leanback.widget.BaseCardView;
+import android.text.Layout;
 import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.View;
+import android.view.ViewTreeObserver;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.ProgressBar;
 import android.widget.TextView;
+import androidx.leanback.widget.BaseCardView;
 import com.android.tv.R;
 import com.android.tv.dvr.data.RecordedProgram;
 import com.android.tv.ui.ViewUtils;
@@ -42,7 +44,7 @@
  */
 public class RecordingCardView extends BaseCardView {
     // This value should be the same with
-    // android.support.v17.leanback.widget.FocusHighlightHelper.BrowseItemFocusHighlight.DURATION_MS
+    // androidx.leanback.widget.FocusHighlightHelper.BrowseItemFocusHighlight.DURATION_MS
     private static final int ANIMATION_DURATION = 150;
     private final ImageView mImageView;
     private final int mImageWidth;
@@ -62,6 +64,7 @@
     private final boolean mExpandTitleWhenFocused;
     private boolean mExpanded;
     private String mDetailBackgroundImageUri;
+    private Layout mTitleViewLayout;
 
     public RecordingCardView(Context context) {
         this(context, false);
@@ -120,6 +123,14 @@
                                                         * value));
                     }
                 });
+        getViewTreeObserver().addOnGlobalLayoutListener(
+                new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                        mTitleViewLayout = mFoldedTitleView.getLayout();
+                    }
+                });
         mExpandTitleWhenFocused = expandTitleWhenFocused;
     }
 
@@ -154,7 +165,8 @@
      * @param withAnimation {@code true} to expand/fold with animation.
      */
     public void expandTitle(boolean expand, boolean withAnimation) {
-        if (expand != mExpanded && mFoldedTitleView.getLayout().getEllipsisCount(0) > 0) {
+        if (expand != mExpanded && mTitleViewLayout != null
+                && mTitleViewLayout.getEllipsisCount(0) > 0) {
             if (withAnimation) {
                 if (expand) {
                     mExpandTitleAnimator.start();
diff --git a/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java
index 243681c..e85f983 100644
--- a/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java
@@ -17,7 +17,7 @@
 package com.android.tv.dvr.ui.browse;
 
 import android.os.Bundle;
-import android.support.v17.leanback.app.DetailsFragment;
+import androidx.leanback.app.DetailsFragment;
 import com.android.tv.TvSingletons;
 import com.android.tv.dvr.data.ScheduledRecording;
 import com.android.tv.ui.DetailsActivity;
diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java
index f08bb12..7ef8e59 100644
--- a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java
@@ -18,9 +18,9 @@
 
 import android.content.res.Resources;
 import android.os.Bundle;
-import android.support.v17.leanback.widget.Action;
-import android.support.v17.leanback.widget.OnActionClickedListener;
-import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import androidx.leanback.widget.Action;
+import androidx.leanback.widget.OnActionClickedListener;
+import androidx.leanback.widget.SparseArrayObjectAdapter;
 
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
diff --git a/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java
index 9104ef1..1c02009 100644
--- a/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java
@@ -21,21 +21,21 @@
 import android.media.tv.TvInputManager;
 import android.os.Bundle;
 import android.support.annotation.Nullable;
-import android.support.v17.leanback.app.DetailsFragment;
-import android.support.v17.leanback.widget.Action;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.ClassPresenterSelector;
-import android.support.v17.leanback.widget.DetailsOverviewRow;
-import android.support.v17.leanback.widget.DetailsOverviewRowPresenter;
-import android.support.v17.leanback.widget.HeaderItem;
-import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.OnActionClickedListener;
-import android.support.v17.leanback.widget.PresenterSelector;
-import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
 import android.text.TextUtils;
+import androidx.leanback.app.DetailsFragment;
+import androidx.leanback.widget.Action;
+import androidx.leanback.widget.ArrayObjectAdapter;
+import androidx.leanback.widget.ClassPresenterSelector;
+import androidx.leanback.widget.DetailsOverviewRow;
+import androidx.leanback.widget.DetailsOverviewRowPresenter;
+import androidx.leanback.widget.HeaderItem;
+import androidx.leanback.widget.ListRow;
+import androidx.leanback.widget.OnActionClickedListener;
+import androidx.leanback.widget.PresenterSelector;
+import androidx.leanback.widget.SparseArrayObjectAdapter;
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
-import com.android.tv.data.BaseProgram;
+import com.android.tv.data.api.BaseProgram;
 import com.android.tv.dvr.DvrDataManager;
 import com.android.tv.dvr.DvrWatchedPositionManager;
 import com.android.tv.dvr.data.RecordedProgram;
diff --git a/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java
index 77a6350..293e1d9 100644
--- a/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java
+++ b/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java
@@ -17,8 +17,8 @@
 package com.android.tv.dvr.ui.list;
 
 import android.os.Bundle;
-import android.support.v17.leanback.app.DetailsFragment;
-import android.support.v17.leanback.widget.ClassPresenterSelector;
+import androidx.leanback.app.DetailsFragment;
+import androidx.leanback.widget.ClassPresenterSelector;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
diff --git a/src/com/android/tv/dvr/ui/list/DvrHistoryFragment.java b/src/com/android/tv/dvr/ui/list/DvrHistoryFragment.java
index 0ca05fa..1d93c8c 100644
--- a/src/com/android/tv/dvr/ui/list/DvrHistoryFragment.java
+++ b/src/com/android/tv/dvr/ui/list/DvrHistoryFragment.java
@@ -17,23 +17,24 @@
 package com.android.tv.dvr.ui.list;
 
 import android.os.Bundle;
-import android.support.v17.leanback.app.DetailsFragment;
-import android.support.v17.leanback.widget.ClassPresenterSelector;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.TextView;
+import androidx.leanback.app.DetailsFragment;
+import androidx.leanback.widget.ClassPresenterSelector;
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
 import com.android.tv.dvr.DvrDataManager;
 import com.android.tv.dvr.data.RecordedProgram;
 import com.android.tv.dvr.data.ScheduledRecording;
 import com.android.tv.dvr.ui.list.SchedulesHeaderRowPresenter.DateHeaderRowPresenter;
+import com.android.tv.common.flags.UiFlags;
 
 /** A fragment to show the DVR history. */
 public class DvrHistoryFragment extends DetailsFragment
         implements DvrDataManager.ScheduledRecordingListener,
-        DvrDataManager.RecordedProgramListener {
+                DvrDataManager.RecordedProgramListener {
 
     private DvrHistoryRowAdapter mRowsAdapter;
     private TextView mEmptyInfoScreenView;
@@ -48,11 +49,17 @@
         presenterSelector.addClassPresenter(
                 ScheduleRow.class, new ScheduleRowPresenter(getContext()));
         TvSingletons singletons = TvSingletons.getSingletons(getContext());
-        mRowsAdapter = new DvrHistoryRowAdapter(
-                getContext(), presenterSelector, singletons.getClock());
+        UiFlags uiFlags = singletons.getUiFlags();
+        mDvrDataManager = singletons.getDvrDataManager();
+        mRowsAdapter =
+                new DvrHistoryRowAdapter(
+                        getContext(),
+                        presenterSelector,
+                        singletons.getClock(),
+                        mDvrDataManager,
+                        uiFlags);
         setAdapter(mRowsAdapter);
         mRowsAdapter.start();
-        mDvrDataManager = singletons.getDvrDataManager();
         mDvrDataManager.addScheduledRecordingListener(this);
         mDvrDataManager.addRecordedProgramListener(this);
         mEmptyInfoScreenView = (TextView) getActivity().findViewById(R.id.empty_info_screen);
@@ -135,7 +142,6 @@
                 hideEmptyMessage();
             }
         }
-
     }
 
     @Override
diff --git a/src/com/android/tv/dvr/ui/list/DvrHistoryRowAdapter.java b/src/com/android/tv/dvr/ui/list/DvrHistoryRowAdapter.java
index 156d1a7..a10367f 100644
--- a/src/com/android/tv/dvr/ui/list/DvrHistoryRowAdapter.java
+++ b/src/com/android/tv/dvr/ui/list/DvrHistoryRowAdapter.java
@@ -20,20 +20,19 @@
 import android.content.Context;
 import android.os.Build.VERSION_CODES;
 import android.support.annotation.Nullable;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.ClassPresenterSelector;
 import android.text.format.DateUtils;
 import android.util.Log;
+import androidx.leanback.widget.ArrayObjectAdapter;
+import androidx.leanback.widget.ClassPresenterSelector;
 import com.android.tv.R;
-import com.android.tv.TvSingletons;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.util.Clock;
 import com.android.tv.dvr.DvrDataManager;
 import com.android.tv.dvr.data.RecordedProgram;
 import com.android.tv.dvr.data.ScheduledRecording;
-import com.android.tv.dvr.recorder.ScheduledProgramReaper;
 import com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow;
 import com.android.tv.util.Utils;
+import com.android.tv.common.flags.UiFlags;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -48,8 +47,8 @@
     private static final boolean DEBUG = false;
 
     private static final long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1);
-    private static final int MAX_HISTORY_DAYS = ScheduledProgramReaper.DAYS;
 
+    private final long mMaxHistoryDays;
     private final Context mContext;
     private final Clock mClock;
     private final DvrDataManager mDvrDataManager;
@@ -57,11 +56,16 @@
     private final Map<Long, ScheduledRecording> mRecordedProgramScheduleMap = new HashMap<>();
 
     public DvrHistoryRowAdapter(
-            Context context, ClassPresenterSelector classPresenterSelector, Clock clock) {
+            Context context,
+            ClassPresenterSelector classPresenterSelector,
+            Clock clock,
+            DvrDataManager dvrDataManager,
+            UiFlags uiFlags) {
         super(classPresenterSelector);
         mContext = context;
         mClock = clock;
-        mDvrDataManager = TvSingletons.getSingletons(mContext).getDvrDataManager();
+        mDvrDataManager = dvrDataManager;
+        mMaxHistoryDays = uiFlags.maxHistoryDays();
         mTitles.add(mContext.getString(R.string.dvr_date_today));
         mTitles.add(mContext.getString(R.string.dvr_date_yesterday));
     }
@@ -78,9 +82,9 @@
         List<RecordedProgram> recordedProgramList = mDvrDataManager.getRecordedPrograms();
 
         recordingList.addAll(
-                recordedProgramsToScheduledRecordings(recordedProgramList, MAX_HISTORY_DAYS));
-        recordingList
-                .sort(ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR.reversed());
+                recordedProgramsToScheduledRecordings(recordedProgramList, mMaxHistoryDays));
+        recordingList.sort(
+                ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR.reversed());
         long deadLine = Utils.getFirstMillisecondOfDay(mClock.currentTimeMillis());
         for (int i = 0; i < recordingList.size(); ) {
             ArrayList<ScheduledRecording> section = new ArrayList<>();
@@ -128,7 +132,7 @@
     }
 
     private List<ScheduledRecording> recordedProgramsToScheduledRecordings(
-            List<RecordedProgram> programs, int maxDays) {
+            List<RecordedProgram> programs, long maxDays) {
         List<ScheduledRecording> result = new ArrayList<>();
         for (RecordedProgram recordedProgram : programs) {
             ScheduledRecording scheduledRecording =
@@ -142,12 +146,12 @@
 
     @Nullable
     private ScheduledRecording recordedProgramsToScheduledRecordings(
-            RecordedProgram program, int maxDays) {
+            RecordedProgram program, long maxDays) {
         long firstMillisecondToday = Utils.getFirstMillisecondOfDay(mClock.currentTimeMillis());
-        if (maxDays
-                < Utils.computeDateDifference(
-                        program.getStartTimeUtcMillis(),
-                        firstMillisecondToday)) {
+        if (maxDays != 0
+                && maxDays
+                        < Utils.computeDateDifference(
+                                program.getStartTimeUtcMillis(), firstMillisecondToday)) {
             return null;
         }
         ScheduledRecording scheduledRecording = ScheduledRecording.builder(program).build();
@@ -175,7 +179,7 @@
             return;
         }
         ScheduledRecording schedule =
-                recordedProgramsToScheduledRecordings(program, MAX_HISTORY_DAYS);
+                recordedProgramsToScheduledRecordings(program, mMaxHistoryDays);
         if (schedule == null) {
             return;
         }
@@ -248,8 +252,10 @@
             for (; index < size(); index++) {
                 if (get(index) instanceof ScheduleRow) {
                     ScheduleRow scheduleRow = (ScheduleRow) get(index);
-                    if (ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR.reversed()
-                            .compare(scheduleRow.getSchedule(), recording) > 0) {
+                    if (ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR
+                                    .reversed()
+                                    .compare(scheduleRow.getSchedule(), recording)
+                            > 0) {
                         break;
                     }
                     pre = index;
diff --git a/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java b/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java
index 82b8563..d0884f9 100644
--- a/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java
+++ b/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java
@@ -20,13 +20,15 @@
 import android.app.ProgressDialog;
 import android.os.Bundle;
 import android.support.annotation.IntDef;
+
 import com.android.tv.R;
 import com.android.tv.Starter;
-import com.android.tv.data.Program;
+import com.android.tv.data.api.Program;
 import com.android.tv.dvr.data.SeriesRecording;
 import com.android.tv.dvr.provider.EpisodicProgramLoadTask;
 import com.android.tv.dvr.recorder.SeriesRecordingScheduler;
 import com.android.tv.dvr.ui.BigArguments;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.Collections;
diff --git a/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java
index d97b61f..43a3579 100644
--- a/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java
+++ b/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java
@@ -17,7 +17,7 @@
 package com.android.tv.dvr.ui.list;
 
 import android.os.Bundle;
-import android.support.v17.leanback.widget.ClassPresenterSelector;
+import androidx.leanback.widget.ClassPresenterSelector;
 import com.android.tv.R;
 import com.android.tv.dvr.data.ScheduledRecording;
 import com.android.tv.dvr.ui.list.SchedulesHeaderRowPresenter.DateHeaderRowPresenter;
diff --git a/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java
index d376e35..50bc04c 100644
--- a/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java
+++ b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java
@@ -25,20 +25,24 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
-import android.support.v17.leanback.widget.ClassPresenterSelector;
 import android.transition.Fade;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+
+import androidx.leanback.widget.ClassPresenterSelector;
+
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
 import com.android.tv.data.ChannelDataManager;
-import com.android.tv.data.Program;
+import com.android.tv.data.ProgramImpl;
+import com.android.tv.data.api.Program;
 import com.android.tv.dvr.DvrDataManager;
 import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener;
 import com.android.tv.dvr.data.SeriesRecording;
 import com.android.tv.dvr.provider.EpisodicProgramLoadTask;
 import com.android.tv.dvr.ui.BigArguments;
+
 import java.util.Collections;
 import java.util.List;
 
@@ -53,7 +57,7 @@
             "series_schedules_key_series_recording";
     /**
      * The key for programs which belong to the series recording whose scheduled recording list will
-     * be displayed. Type: List<{@link Program}>
+     * be displayed. Type: List<{@link ProgramImpl}>
      */
     public static final String SERIES_SCHEDULES_KEY_SERIES_PROGRAMS =
             "series_schedules_key_series_programs";
diff --git a/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java b/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java
index d580841..ccb497f 100644
--- a/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java
+++ b/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java
@@ -17,7 +17,8 @@
 package com.android.tv.dvr.ui.list;
 
 import android.content.Context;
-import com.android.tv.data.Program;
+
+import com.android.tv.data.api.Program;
 import com.android.tv.dvr.data.ScheduledRecording;
 import com.android.tv.dvr.data.ScheduledRecording.Builder;
 import com.android.tv.dvr.ui.DvrUiHelper;
diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java
index ef4a433..de259f5 100644
--- a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java
+++ b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java
@@ -22,8 +22,8 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.ClassPresenterSelector;
+import androidx.leanback.widget.ArrayObjectAdapter;
+import androidx.leanback.widget.ClassPresenterSelector;
 import android.text.format.DateUtils;
 import android.util.ArraySet;
 import android.util.Log;
diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
index 11680a0..ff296f4 100644
--- a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
+++ b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
@@ -24,7 +24,7 @@
 import android.content.res.Resources;
 import android.os.Build;
 import android.support.annotation.IntDef;
-import android.support.v17.leanback.widget.RowPresenter;
+import androidx.leanback.widget.RowPresenter;
 import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.View;
diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java
index bbddc07..5c687cd 100644
--- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java
+++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java
@@ -16,8 +16,9 @@
 
 package com.android.tv.dvr.ui.list;
 
-import com.android.tv.data.Program;
+import com.android.tv.data.api.Program;
 import com.android.tv.dvr.data.SeriesRecording;
+
 import java.util.List;
 
 /** A base class for the rows for schedules' header. */
diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
index 28a44bf..2550eeb 100644
--- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
+++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
@@ -19,7 +19,7 @@
 import android.animation.ValueAnimator;
 import android.content.Context;
 import android.graphics.drawable.Drawable;
-import android.support.v17.leanback.widget.RowPresenter;
+import androidx.leanback.widget.RowPresenter;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.OnClickListener;
diff --git a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java
index 9a9c94e..2c37754 100644
--- a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java
+++ b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java
@@ -20,19 +20,22 @@
 import android.content.Context;
 import android.media.tv.TvInputInfo;
 import android.os.Build;
-import android.support.v17.leanback.widget.ClassPresenterSelector;
 import android.util.ArrayMap;
 import android.util.Log;
+
+import androidx.leanback.widget.ClassPresenterSelector;
+
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
 import com.android.tv.common.SoftPreconditions;
-import com.android.tv.data.Program;
+import com.android.tv.data.api.Program;
 import com.android.tv.dvr.DvrDataManager;
 import com.android.tv.dvr.DvrManager;
 import com.android.tv.dvr.data.ScheduledRecording;
 import com.android.tv.dvr.data.SeriesRecording;
 import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow;
 import com.android.tv.util.Utils;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java
index f24ad2c..4aa1200 100644
--- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java
@@ -16,7 +16,6 @@
 
 package com.android.tv.dvr.ui.playback;
 
-import android.app.Activity;
 import android.content.ContentUris;
 import android.content.Intent;
 import android.content.res.Configuration;
@@ -28,9 +27,12 @@
 import com.android.tv.dialog.PinDialogFragment.OnPinCheckedListener;
 import com.android.tv.dvr.data.RecordedProgram;
 import com.android.tv.util.Utils;
+import dagger.android.AndroidInjection;
+import dagger.android.ContributesAndroidInjector;
+import dagger.android.DaggerActivity;
 
 /** Activity to play a {@link RecordedProgram}. */
-public class DvrPlaybackActivity extends Activity implements OnPinCheckedListener {
+public class DvrPlaybackActivity extends DaggerActivity implements OnPinCheckedListener {
     private static final String TAG = "DvrPlaybackActivity";
     private static final boolean DEBUG = false;
 
@@ -39,6 +41,7 @@
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
+        AndroidInjection.inject(this);
         Starter.start(this);
         if (DEBUG) Log.d(TAG, "onCreate");
         super.onCreate(savedInstanceState);
@@ -92,4 +95,16 @@
     void setOnPinCheckListener(OnPinCheckedListener listener) {
         mOnPinCheckedListener = listener;
     }
+
+    /**
+     * Exports {@link DvrPlaybackActivity} for Dagger codegen to create the appropriate injector.
+     */
+    @dagger.Module
+    public abstract static class Module {
+        @ContributesAndroidInjector
+        abstract DvrPlaybackActivity contributesDvrPlaybackActivity();
+
+        @ContributesAndroidInjector
+        abstract DvrPlaybackOverlayFragment contributesDvrPlaybackOverlayFragment();
+    }
 }
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java
index 791d26b..35c5d4e 100644
--- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java
@@ -26,15 +26,15 @@
 import android.media.tv.TvTrackInfo;
 import android.os.Bundle;
 import android.support.annotation.Nullable;
-import android.support.v17.leanback.media.PlaybackControlGlue;
-import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter;
-import android.support.v17.leanback.widget.Action;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.PlaybackControlsRow;
-import android.support.v17.leanback.widget.PlaybackControlsRow.ClosedCaptioningAction;
-import android.support.v17.leanback.widget.PlaybackControlsRow.MultiAction;
-import android.support.v17.leanback.widget.PlaybackControlsRowPresenter;
-import android.support.v17.leanback.widget.RowPresenter;
+import androidx.leanback.media.PlaybackControlGlue;
+import androidx.leanback.widget.AbstractDetailsDescriptionPresenter;
+import androidx.leanback.widget.Action;
+import androidx.leanback.widget.ArrayObjectAdapter;
+import androidx.leanback.widget.PlaybackControlsRow;
+import androidx.leanback.widget.PlaybackControlsRow.ClosedCaptioningAction;
+import androidx.leanback.widget.PlaybackControlsRow.MultiAction;
+import androidx.leanback.widget.PlaybackControlsRowPresenter;
+import androidx.leanback.widget.RowPresenter;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.KeyEvent;
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java
index 1059e85..0c96cac 100644
--- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java
@@ -26,24 +26,25 @@
 import android.media.tv.TvInputManager;
 import android.media.tv.TvTrackInfo;
 import android.os.Bundle;
-import android.support.v17.leanback.app.PlaybackFragment;
-import android.support.v17.leanback.app.PlaybackFragmentGlueHost;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.BaseOnItemViewClickedListener;
-import android.support.v17.leanback.widget.ClassPresenterSelector;
-import android.support.v17.leanback.widget.HeaderItem;
-import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.RowPresenter;
-import android.support.v17.leanback.widget.SinglePresenterSelector;
 import android.util.Log;
 import android.view.Display;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.Toast;
+import androidx.leanback.app.PlaybackFragment;
+import androidx.leanback.app.PlaybackFragmentGlueHost;
+import androidx.leanback.widget.ArrayObjectAdapter;
+import androidx.leanback.widget.BaseOnItemViewClickedListener;
+import androidx.leanback.widget.ClassPresenterSelector;
+import androidx.leanback.widget.HeaderItem;
+import androidx.leanback.widget.ListRow;
+import androidx.leanback.widget.Presenter;
+import androidx.leanback.widget.RowPresenter;
+import androidx.leanback.widget.SinglePresenterSelector;
 import com.android.tv.R;
-import com.android.tv.TvSingletons;
-import com.android.tv.data.BaseProgram;
+import com.android.tv.audio.AudioManagerHelper;
+import com.android.tv.common.buildtype.HasBuildType.BuildType;
+import com.android.tv.data.api.BaseProgram;
 import com.android.tv.dialog.PinDialogFragment;
 import com.android.tv.dvr.DvrDataManager;
 import com.android.tv.dvr.data.RecordedProgram;
@@ -55,8 +56,11 @@
 import com.android.tv.util.TvSettings;
 import com.android.tv.util.TvTrackInfoUtils;
 import com.android.tv.util.Utils;
+import dagger.android.AndroidInjection;
+import com.android.tv.common.flags.LegacyFlags;
 import java.util.ArrayList;
 import java.util.List;
+import javax.inject.Inject;
 
 public class DvrPlaybackOverlayFragment extends PlaybackFragment {
     // TODO: Handles audio focus. Deals with block and ratings.
@@ -75,7 +79,7 @@
     private ArrayObjectAdapter mRowsAdapter;
     private SortedArrayAdapter<BaseProgram> mRelatedRecordingsRowAdapter;
     private DvrPlaybackCardPresenter mRelatedRecordingCardPresenter;
-    private DvrDataManager mDvrDataManager;
+    private AudioManagerHelper mAudioManagerHelper;
     private AppLayerTvView mTvView;
     private View mBlockScreenView;
     private ListRow mRelatedRecordingsRow;
@@ -97,9 +101,24 @@
                 }
             };
 
+    @Inject DvrDataManager mDvrDataManager;
+    @Inject LegacyFlags mLegacyFlags;
+    @Inject BuildType buildType;
+
+    @Override
+    public void onAttach(Context context) {
+        if (DEBUG) {
+            Log.d(TAG, "onAttach");
+        }
+        AndroidInjection.inject(this);
+        super.onAttach(context);
+    }
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
-        if (DEBUG) Log.d(TAG, "onCreate");
+        if (DEBUG) {
+            Log.d(TAG, "onCreate");
+        }
         super.onCreate(savedInstanceState);
         mVerticalPaddingBase =
                 getActivity()
@@ -115,7 +134,6 @@
                         .getResources()
                         .getDimensionPixelOffset(
                                 R.dimen.dvr_playback_overlay_padding_top_no_secondary_row);
-        mDvrDataManager = TvSingletons.getSingletons(getActivity()).getDvrDataManager();
         if (!mDvrDataManager.isRecordedProgramLoadFinished()) {
             mDvrDataManager.addRecordedProgramLoadFinishedListener(
                     new DvrDataManager.OnRecordedProgramLoadFinishedListener() {
@@ -153,6 +171,8 @@
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
         mTvView = getActivity().findViewById(R.id.dvr_tv_view);
+        mTvView.setUseSecureSurface(
+                buildType != BuildType.ENG && !mLegacyFlags.enableDeveloperFeatures());
         mBlockScreenView = getActivity().findViewById(R.id.block_screen);
         mDvrPlayer = new DvrPlayer(mTvView, getActivity());
         mMediaSessionHelper =
@@ -240,13 +260,16 @@
                             setFadingEnabled(false);
                             long programId =
                                     ((RecordedProgram) itemViewHolder.view.getTag()).getId();
-                            if (DEBUG) Log.d(TAG, "Play Related Recording:" + programId);
+                            if (DEBUG) {
+                                Log.d(TAG, "Play Related Recording:" + programId);
+                            }
                             Intent intent = new Intent(getContext(), DvrPlaybackActivity.class);
                             intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, programId);
                             getContext().startActivity(intent);
                         }
                     }
                 });
+        mAudioManagerHelper = new AudioManagerHelper(getActivity(), mDvrPlayer.getView());
         if (mProgram != null) {
             setUpRows();
             preparePlayback(getActivity().getIntent());
@@ -255,7 +278,9 @@
 
     @Override
     public void onPause() {
-        if (DEBUG) Log.d(TAG, "onPause");
+        if (DEBUG) {
+            Log.d(TAG, "onPause");
+        }
         super.onPause();
         if (mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_FAST_FORWARDING
                 || mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_REWINDING) {
@@ -270,9 +295,12 @@
 
     @Override
     public void onDestroy() {
-        if (DEBUG) Log.d(TAG, "onDestroy");
+        if (DEBUG) {
+            Log.d(TAG, "onDestroy");
+        }
         mPlaybackControlHelper.unregisterCallback();
         mMediaSessionHelper.release();
+        mAudioManagerHelper.abandonAudioFocus();
         mRelatedRecordingCardPresenter.unbindAllViewHolders();
         mDvrPlayer.release();
         super.onDestroy();
@@ -416,6 +444,7 @@
     private void preparePlayback(Intent intent) {
         mMediaSessionHelper.setupPlayback(mProgram, getSeekTimeFromIntent(intent));
         mPlaybackControlHelper.updateSecondaryRow(false, false);
+        mAudioManagerHelper.requestAudioFocus();
         getActivity().getMediaController().getTransportControls().prepare();
         updateRelatedRecordingsRow();
     }
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java
index b4481df..95858e3 100644
--- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java
@@ -19,8 +19,8 @@
 import android.media.tv.TvTrackInfo;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
-import android.support.v17.leanback.app.GuidedStepFragment;
-import android.support.v17.leanback.widget.GuidedAction;
+import androidx.leanback.app.GuidedStepFragment;
+import androidx.leanback.widget.GuidedAction;
 import android.text.TextUtils;
 import android.transition.Transition;
 import android.view.LayoutInflater;
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlayer.java b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java
index d14646b..e8325e1 100644
--- a/src/com/android/tv/dvr/ui/playback/DvrPlayer.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java
@@ -326,6 +326,11 @@
         return mProgram;
     }
 
+    /** Returns the DVR tv view. */
+    public DvrTvView getView() {
+        return mTvView;
+    }
+
     /** Returns the currrent playback posistion in msecs. */
     public long getPlaybackPosition() {
         return mTimeShiftCurrentPositionMs;
diff --git a/src/com/android/tv/features/TvFeatures.java b/src/com/android/tv/features/TvFeatures.java
index 208d53f..f7a39ef 100644
--- a/src/com/android/tv/features/TvFeatures.java
+++ b/src/com/android/tv/features/TvFeatures.java
@@ -27,14 +27,11 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.os.Build;
-import android.support.annotation.VisibleForTesting;
-import com.android.tv.common.experiments.Experiments;
+
 import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.common.feature.ExperimentFeature;
 import com.android.tv.common.feature.Feature;
 import com.android.tv.common.feature.FeatureUtils;
 import com.android.tv.common.feature.FlagFeature;
-import com.android.tv.common.feature.PropertyFeature;
 import com.android.tv.common.feature.Sdk;
 import com.android.tv.common.feature.TestableFeature;
 import com.android.tv.common.flags.has.HasUiFlags;
@@ -42,7 +39,7 @@
 import com.android.tv.common.util.PermissionUtils;
 
 /**
- * List of {@link Feature} for the Live TV App.
+ * List of {@link Feature} for the TV app.
  *
  * <p>Remove the {@code Feature} once it is launched.
  */
@@ -51,16 +48,6 @@
     /** When enabled store network affiliation information to TV provider */
     public static final Feature STORE_NETWORK_AFFILIATION = ENG_ONLY_FEATURE;
 
-    /** When enabled use system setting for turning on analytics. */
-    public static final Feature ANALYTICS_OPT_IN =
-            ExperimentFeature.from(Experiments.ENABLE_ANALYTICS_VIA_CHECKBOX);
-    /**
-     * Analytics that include sensitive information such as channel or program identifiers.
-     *
-     * <p>See <a href="http://b/22062676">b/22062676</a>
-     */
-    public static final Feature ANALYTICS_V2 = and(ON, ANALYTICS_OPT_IN);
-
     private static final Feature TV_PROVIDER_ALLOWS_INSERT_TO_PROGRAM_TABLE =
             or(Sdk.AT_LEAST_O, PartnerFeatures.TVPROVIDER_ALLOWS_SYSTEM_INSERTS_TO_PROGRAM_TABLE);
 
@@ -114,8 +101,5 @@
     /** Use input blacklist to disable partner's tuner input. */
     public static final Feature USE_PARTNER_INPUT_BLACKLIST = ON;
 
-    @VisibleForTesting
-    public static final Feature TEST_FEATURE = PropertyFeature.create("test_feature", false);
-
     private TvFeatures() {}
 }
diff --git a/src/com/android/tv/guide/GenreListAdapter.java b/src/com/android/tv/guide/GenreListAdapter.java
index b4baf42..995b053 100644
--- a/src/com/android/tv/guide/GenreListAdapter.java
+++ b/src/com/android/tv/guide/GenreListAdapter.java
@@ -18,7 +18,7 @@
 
 import android.content.Context;
 import android.support.annotation.MainThread;
-import android.support.v7.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
diff --git a/src/com/android/tv/guide/ProgramGrid.java b/src/com/android/tv/guide/ProgramGrid.java
index caafb04..96e161c 100644
--- a/src/com/android/tv/guide/ProgramGrid.java
+++ b/src/com/android/tv/guide/ProgramGrid.java
@@ -19,7 +19,7 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Rect;
-import android.support.v17.leanback.widget.VerticalGridView;
+import androidx.leanback.widget.VerticalGridView;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.Range;
@@ -256,8 +256,19 @@
                 scrollToPosition(getAdapter().getItemCount() - 1);
                 return null;
             } else if (getSelectedPosition() == getAdapter().getItemCount() - 1) {
-                scrollToPosition(0);
-                return null;
+                int itemCount = getLayoutManager().getItemCount();
+                int childCount = getChildCount();
+                // b/129466363 For an item which overalps with previous layout GridLayoutManager
+                // will scroll to first child of current layout, instead of going to previous one.
+                // smoothscrollToPosition will invalidate all layouts and scroll to position 0.
+                // This condition checks for an item which overlaps with the first layout
+                if (itemCount > 2 * (childCount + 1) || itemCount <= childCount) {
+                    scrollToPosition(0);
+                    return null;
+                } else {
+                    smoothScrollToPosition(0);
+                    return getChildAt(0);
+                }
             }
             return focused;
         }
diff --git a/src/com/android/tv/guide/ProgramGuide.java b/src/com/android/tv/guide/ProgramGuide.java
index bc1b11b..8ae61e8 100644
--- a/src/com/android/tv/guide/ProgramGuide.java
+++ b/src/com/android/tv/guide/ProgramGuide.java
@@ -32,10 +32,7 @@
 import android.preference.PreferenceManager;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
-import android.support.v17.leanback.widget.OnChildSelectedListener;
-import android.support.v17.leanback.widget.SearchOrbView;
-import android.support.v17.leanback.widget.VerticalGridView;
-import android.support.v7.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView;
 import android.util.Log;
 import android.view.View;
 import android.view.View.MeasureSpec;
@@ -44,6 +41,11 @@
 import android.view.ViewTreeObserver;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
+
+import androidx.leanback.widget.OnChildSelectedListener;
+import androidx.leanback.widget.SearchOrbView;
+import androidx.leanback.widget.VerticalGridView;
+
 import com.android.tv.ChannelTuner;
 import com.android.tv.MainActivity;
 import com.android.tv.R;
@@ -65,7 +67,9 @@
 import com.android.tv.ui.hideable.AutoHideScheduler;
 import com.android.tv.util.TvInputManagerHelper;
 import com.android.tv.util.Utils;
-import com.android.tv.common.flags.BackendKnobsFlags;
+
+import com.android.tv.common.flags.UiFlags;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
@@ -112,6 +116,7 @@
     private final int mAnimationDuration;
     private final int mDetailPadding;
     private final SearchOrbView mSearchOrb;
+    private final UiFlags mUiFlags;
     private int mCurrentTimeIndicatorWidth;
 
     private final View mContainer;
@@ -185,15 +190,14 @@
         mActivity = activity;
         TvSingletons singletons = TvSingletons.getSingletons(mActivity);
         mPerformanceMonitor = singletons.getPerformanceMonitor();
-        BackendKnobsFlags backendKnobsFlags = singletons.getBackendKnobs();
+        mUiFlags = singletons.getUiFlags();
         mProgramManager =
                 new ProgramManager(
                         tvInputManagerHelper,
                         channelDataManager,
                         programDataManager,
                         dvrDataManager,
-                        dvrScheduleManager,
-                        backendKnobsFlags);
+                        dvrScheduleManager);
         mChannelTuner = channelTuner;
         mTracker = tracker;
         mPreShowRunnable = preShowRunnable;
@@ -261,7 +265,7 @@
                         }
                     });
             mSidePanelGridView.setOnChildSelectedListener(
-                    new android.support.v17.leanback.widget.OnChildSelectedListener() {
+                    new androidx.leanback.widget.OnChildSelectedListener() {
                         @Override
                         public void onChildSelected(ViewGroup viewGroup, View view, int i, long l) {
                             mSearchOrb.animate().alpha(i == 0 ? 1.0f : 0.0f);
@@ -282,7 +286,8 @@
                         res.getInteger(R.integer.max_recycled_view_pool_epg_header_row_item));
         mTimelineRow.setAdapter(mTimeListAdapter);
 
-        ProgramTableAdapter programTableAdapter = new ProgramTableAdapter(mActivity, this);
+        ProgramTableAdapter programTableAdapter =
+                new ProgramTableAdapter(mActivity, this, mUiFlags);
         programTableAdapter.registerAdapterDataObserver(
                 new RecyclerView.AdapterDataObserver() {
                     @Override
diff --git a/src/com/android/tv/guide/ProgramItemView.java b/src/com/android/tv/guide/ProgramItemView.java
index a46beab..65b7641 100644
--- a/src/com/android/tv/guide/ProgramItemView.java
+++ b/src/com/android/tv/guide/ProgramItemView.java
@@ -23,6 +23,7 @@
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.LayerDrawable;
 import android.graphics.drawable.StateListDrawable;
+import android.os.Build;
 import android.os.Handler;
 import android.text.SpannableStringBuilder;
 import android.text.Spanned;
@@ -34,6 +35,7 @@
 import android.view.ViewGroup;
 import android.widget.TextView;
 import android.widget.Toast;
+
 import com.android.tv.MainActivity;
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
@@ -41,17 +43,22 @@
 import com.android.tv.common.feature.CommonFeatures;
 import com.android.tv.common.util.Clock;
 import com.android.tv.data.ChannelDataManager;
-import com.android.tv.data.Program;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 import com.android.tv.dvr.DvrManager;
 import com.android.tv.dvr.data.ScheduledRecording;
 import com.android.tv.dvr.ui.DvrUiHelper;
 import com.android.tv.guide.ProgramManager.TableEntry;
 import com.android.tv.util.ToastUtils;
 import com.android.tv.util.Utils;
+
+import dagger.android.HasAndroidInjector;
+
 import java.lang.reflect.InvocationTargetException;
 import java.util.concurrent.TimeUnit;
 
+import javax.inject.Inject;
+
 public class ProgramItemView extends TextView {
     private static final String TAG = "ProgramItemView";
 
@@ -73,8 +80,8 @@
     private static TextAppearanceSpan sGrayedOutEpisodeTitleStyle;
 
     private final DvrManager mDvrManager;
-    private final Clock mClock;
-    private final ChannelDataManager mChannelDataManager;
+    @Inject Clock mClock;
+    @Inject ChannelDataManager mChannelDataManager;
     private ProgramGuide mProgramGuide;
     private TableEntry mTableEntry;
     private int mMaxWidthForRipple;
@@ -202,12 +209,11 @@
 
     public ProgramItemView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
+        ((HasAndroidInjector) context).androidInjector().inject(this);
         setOnClickListener(ON_CLICKED);
         setOnFocusChangeListener(ON_FOCUS_CHANGED);
         TvSingletons singletons = TvSingletons.getSingletons(getContext());
         mDvrManager = singletons.getDvrManager();
-        mChannelDataManager = singletons.getChannelDataManager();
-        mClock = singletons.getClock();
     }
 
     private void initIfNeeded() {
@@ -530,6 +536,11 @@
     }
 
     private static int getStateCount(StateListDrawable stateListDrawable) {
+        /* Begin_AOSP_Before_Q_Comment_Out */
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            return stateListDrawable.getStateCount();
+        }
+        /* End_AOSP_Before_Q_Comment_Out */
         try {
             Object stateCount =
                     StateListDrawable.class
@@ -546,6 +557,11 @@
     }
 
     private static Drawable getStateDrawable(StateListDrawable stateListDrawable, int index) {
+        /* Begin_AOSP_Before_Q_Comment_Out */
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            return stateListDrawable.getStateDrawable(index);
+        }
+        /* End_AOSP_Before_Q_Comment_Out */
         try {
             Object drawable =
                     StateListDrawable.class
diff --git a/src/com/android/tv/guide/ProgramListAdapter.java b/src/com/android/tv/guide/ProgramListAdapter.java
index 397bacf..68ae43e 100644
--- a/src/com/android/tv/guide/ProgramListAdapter.java
+++ b/src/com/android/tv/guide/ProgramListAdapter.java
@@ -17,7 +17,7 @@
 package com.android.tv.guide;
 
 import android.content.res.Resources;
-import android.support.v7.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
diff --git a/src/com/android/tv/guide/ProgramManager.java b/src/com/android/tv/guide/ProgramManager.java
index 3a5a4a0..516a4d9 100644
--- a/src/com/android/tv/guide/ProgramManager.java
+++ b/src/com/android/tv/guide/ProgramManager.java
@@ -21,18 +21,20 @@
 import android.support.annotation.VisibleForTesting;
 import android.util.ArraySet;
 import android.util.Log;
+
 import com.android.tv.data.ChannelDataManager;
 import com.android.tv.data.GenreItems;
-import com.android.tv.data.Program;
 import com.android.tv.data.ProgramDataManager;
+import com.android.tv.data.ProgramImpl;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 import com.android.tv.dvr.DvrDataManager;
 import com.android.tv.dvr.DvrScheduleManager;
 import com.android.tv.dvr.DvrScheduleManager.OnConflictStateChangeListener;
 import com.android.tv.dvr.data.ScheduledRecording;
 import com.android.tv.util.TvInputManagerHelper;
 import com.android.tv.util.Utils;
-import com.android.tv.common.flags.BackendKnobsFlags;
+
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -60,7 +62,6 @@
     private final ProgramDataManager mProgramDataManager;
     private final DvrDataManager mDvrDataManager; // Only set if DVR is enabled
     private final DvrScheduleManager mDvrScheduleManager;
-    private final BackendKnobsFlags mBackendKnobsFlags;
 
     private long mStartUtcMillis;
     private long mEndUtcMillis;
@@ -124,16 +125,8 @@
                 }
 
                 @Override
-                public void onSingleChannelUpdated(long channelId) {
-                    boolean parentalControlsEnabled =
-                            mTvInputManagerHelper
-                                    .getParentalControlSettings()
-                                    .isParentalControlsEnabled();
-                    // Inline the updating of the mChannelIdEntriesMap here so we can only call
-                    // getParentalControlSettings once.
-                    List<TableEntry> entries =
-                            createProgramEntries(channelId, parentalControlsEnabled);
-                    mChannelIdEntriesMap.put(channelId, entries);
+                public void onChannelUpdated() {
+                    updateTableEntriesWithoutNotification(false);
                     notifyTableEntriesUpdated();
                 }
             };
@@ -215,14 +208,12 @@
             ChannelDataManager channelDataManager,
             ProgramDataManager programDataManager,
             @Nullable DvrDataManager dvrDataManager,
-            @Nullable DvrScheduleManager dvrScheduleManager,
-            BackendKnobsFlags backendKnobsFlags) {
+            @Nullable DvrScheduleManager dvrScheduleManager) {
         mTvInputManagerHelper = tvInputManagerHelper;
         mChannelDataManager = channelDataManager;
         mProgramDataManager = programDataManager;
         mDvrDataManager = dvrDataManager;
         mDvrScheduleManager = dvrScheduleManager;
-        mBackendKnobsFlags = backendKnobsFlags;
     }
 
     void programGuideVisibilityChanged(boolean visible) {
@@ -251,7 +242,6 @@
                 mDvrScheduleManager.removeOnConflictStateChangeListener(
                         mOnConflictStateChangeListener);
             }
-            mChannelIdEntriesMap.clear();
         }
     }
 
@@ -426,14 +416,12 @@
     }
 
     /**
-     * Returns an entry as {@link Program} for a given {@code channelId} and {@code index} of
-     * entries within the currently managed time range. Returned {@link Program} can be a dummy one
-     * (e.g., whose channelId is INVALID_ID), when it corresponds to a gap between programs.
+     * Returns an entry as {@link ProgramImpl} for a given {@code channelId} and {@code index} of
+     * entries within the currently managed time range. Returned {@link ProgramImpl} can be a dummy
+     * one (e.g., whose channelId is INVALID_ID), when it corresponds to a gap between programs.
      */
     TableEntry getTableEntry(long channelId, int index) {
-        if (mBackendKnobsFlags.enablePartialProgramFetch()) {
-            mProgramDataManager.prefetchChannel(channelId);
-        }
+        mProgramDataManager.prefetchChannel(channelId, index);
         return mChannelIdEntriesMap.get(channelId).get(index);
     }
 
@@ -707,7 +695,7 @@
     /**
      * Entry for program guide table. An "entry" can be either an actual program or a gap between
      * programs. This is needed for {@link ProgramListAdapter} because {@link
-     * android.support.v17.leanback.widget.HorizontalGridView} ignores margins between items.
+     * androidx.leanback.widget.HorizontalGridView} ignores margins between items.
      */
     static class TableEntry {
         /** Channel ID which this entry is included. */
@@ -737,7 +725,7 @@
 
         private TableEntry(
                 long channelId,
-                Program program,
+                ProgramImpl program,
                 long entryStartUtcMillis,
                 long entryEndUtcMillis,
                 boolean isBlocked) {
@@ -759,7 +747,7 @@
             mIsBlocked = isBlocked;
         }
 
-        /** A stable id useful for {@link android.support.v7.widget.RecyclerView.Adapter}. */
+        /** A stable id useful for {@link androidx.recyclerview.widget.RecyclerView.Adapter}. */
         long getId() {
             // using a negative entryEndUtcMillis keeps it from conflicting with program Id
             return program != null ? program.getId() : -entryEndUtcMillis;
diff --git a/src/com/android/tv/guide/ProgramRow.java b/src/com/android/tv/guide/ProgramRow.java
index 3317c15..6f8f31c 100644
--- a/src/com/android/tv/guide/ProgramRow.java
+++ b/src/com/android/tv/guide/ProgramRow.java
@@ -18,7 +18,7 @@
 
 import android.content.Context;
 import android.graphics.Rect;
-import android.support.v7.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.Range;
diff --git a/src/com/android/tv/guide/ProgramRowAccessibilityDelegate.java b/src/com/android/tv/guide/ProgramRowAccessibilityDelegate.java
index 5e498be..a6a4624 100644
--- a/src/com/android/tv/guide/ProgramRowAccessibilityDelegate.java
+++ b/src/com/android/tv/guide/ProgramRowAccessibilityDelegate.java
@@ -17,8 +17,8 @@
 package com.android.tv.guide;
 
 import android.os.Bundle;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.RecyclerViewAccessibilityDelegate;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityEvent;
diff --git a/src/com/android/tv/guide/ProgramTableAdapter.java b/src/com/android/tv/guide/ProgramTableAdapter.java
index 7576bf5..aed8b90 100644
--- a/src/com/android/tv/guide/ProgramTableAdapter.java
+++ b/src/com/android/tv/guide/ProgramTableAdapter.java
@@ -28,8 +28,8 @@
 import android.os.Handler;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.RecyclerView.RecycledViewPool;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.RecycledViewPool;
 import android.text.Html;
 import android.text.Spannable;
 import android.text.SpannableString;
@@ -47,13 +47,14 @@
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
+
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
 import com.android.tv.common.feature.CommonFeatures;
 import com.android.tv.common.util.CommonUtils;
-import com.android.tv.data.Program;
-import com.android.tv.data.Program.CriticScore;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
+import com.android.tv.data.api.Program.CriticScore;
 import com.android.tv.dvr.DvrDataManager;
 import com.android.tv.dvr.DvrManager;
 import com.android.tv.dvr.data.ScheduledRecording;
@@ -67,6 +68,8 @@
 import com.android.tv.util.images.ImageLoader.ImageLoaderCallback;
 import com.android.tv.util.images.ImageLoader.LoadTvInputLogoTask;
 
+import com.android.tv.common.flags.UiFlags;
+
 import java.util.ArrayList;
 import java.util.List;
 
@@ -109,10 +112,11 @@
     private final String mRecordingInProgressText;
     private final int mDvrPaddingStartWithTrack;
     private final int mDvrPaddingStartWithOutTrack;
+    private final UiFlags mUiFlags;
 
     private RecyclerView mRecyclerView;
 
-    ProgramTableAdapter(Context context, ProgramGuide programGuide) {
+    ProgramTableAdapter(Context context, ProgramGuide programGuide, UiFlags uiFlags) {
         mContext = context;
         mAccessibilityManager =
                 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
@@ -126,6 +130,7 @@
         }
         mProgramGuide = programGuide;
         mProgramManager = programGuide.getProgramManager();
+        mUiFlags = uiFlags;
 
         Resources res = context.getResources();
         mChannelLogoWidth =
@@ -656,6 +661,35 @@
                     mDvrIndicator.setVisibility(View.GONE);
                 }
 
+                if (mUiFlags.enableCriticRatings()) {
+                    // display critic scores if any exist
+                    List<CriticScore> criticScores = program.getCriticScores();
+                    if (criticScores != null) {
+                        // inflate more critic score views if required
+                        if (criticScores.size() > mCriticScoreViews.size()) {
+                            LayoutInflater inflater = LayoutInflater.from(mContext);
+                            LinearLayout layout =
+                                    (LinearLayout)
+                                            inflater.inflate(
+                                                    R.layout.program_guide_critic_score_layout,
+                                                    null);
+                            mCriticScoreViews.add(layout);
+                        }
+                        // fill critic score views and add to layout
+                        for (int i = 0; i < criticScores.size(); i++) {
+                            View criticScoreView = mCriticScoreViews.get(i);
+                            ViewParent previousParentView = criticScoreView.getParent();
+                            if (previousParentView != null
+                                    && previousParentView instanceof ViewGroup) {
+                                ((ViewGroup) previousParentView).removeView(criticScoreView);
+                            }
+                            updateCriticScoreView(
+                                    this, program.getId(), criticScores.get(i), criticScoreView);
+                            mCriticScoresLayout.addView(mCriticScoreViews.get(i));
+                        }
+                    }
+                }
+
                 if (blockedRating == null) {
                     mBlockView.setVisibility(View.GONE);
                     updateTextView(mDescriptionView, program.getDescription());
diff --git a/src/com/android/tv/guide/TimeListAdapter.java b/src/com/android/tv/guide/TimeListAdapter.java
index 9c10c95..62fec69 100644
--- a/src/com/android/tv/guide/TimeListAdapter.java
+++ b/src/com/android/tv/guide/TimeListAdapter.java
@@ -17,7 +17,7 @@
 package com.android.tv.guide;
 
 import android.content.res.Resources;
-import android.support.v7.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView;
 import android.text.format.DateFormat;
 import android.view.LayoutInflater;
 import android.view.View;
diff --git a/src/com/android/tv/guide/TimelineGridView.java b/src/com/android/tv/guide/TimelineGridView.java
index c4922b7..2d25787 100644
--- a/src/com/android/tv/guide/TimelineGridView.java
+++ b/src/com/android/tv/guide/TimelineGridView.java
@@ -17,8 +17,8 @@
 package com.android.tv.guide;
 
 import android.content.Context;
-import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
 import android.util.AttributeSet;
 import android.view.View;
 
diff --git a/src/com/android/tv/menu/ActionCardView.java b/src/com/android/tv/menu/ActionCardView.java
index 3ecd5f5..0e789c6 100644
--- a/src/com/android/tv/menu/ActionCardView.java
+++ b/src/com/android/tv/menu/ActionCardView.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.ImageView;
 import android.widget.RelativeLayout;
 import android.widget.TextView;
@@ -93,6 +94,13 @@
         }
     }
 
+    /** Request focus and accessibility focus on card view. */
+    @Override
+    public boolean requestFocusWithAccessibility() {
+        return requestFocus() &&
+                performAccessibilityAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
+    }
+
     @Override
     public void onRecycled() {}
 }
diff --git a/src/com/android/tv/menu/AppLinkCardView.java b/src/com/android/tv/menu/AppLinkCardView.java
index fd93c31..49d32fe 100644
--- a/src/com/android/tv/menu/AppLinkCardView.java
+++ b/src/com/android/tv/menu/AppLinkCardView.java
@@ -26,13 +26,13 @@
 import android.graphics.drawable.Drawable;
 import android.os.AsyncTask;
 import android.support.annotation.Nullable;
-import android.support.v7.graphics.Palette;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.View;
 import android.widget.ImageView;
 import android.widget.TextView;
+import androidx.palette.graphics.Palette;
 import com.android.tv.MainActivity;
 import com.android.tv.R;
 import com.android.tv.data.api.Channel;
diff --git a/src/com/android/tv/menu/BaseCardView.java b/src/com/android/tv/menu/BaseCardView.java
index 3a94ebb..ed78cb7 100644
--- a/src/com/android/tv/menu/BaseCardView.java
+++ b/src/com/android/tv/menu/BaseCardView.java
@@ -27,6 +27,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewOutlineProvider;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 import com.android.tv.R;
@@ -135,6 +136,13 @@
         }
     }
 
+    /** Request focus and accessibility focus on card view. */
+    @Override
+    public boolean requestFocusWithAccessibility() {
+        return requestFocus() &&
+                performAccessibilityAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
+    }
+
     /** Sets text of this card view. */
     public void setText(int resId) {
         if (mTextResId != resId) {
diff --git a/src/com/android/tv/menu/ChannelCardView.java b/src/com/android/tv/menu/ChannelCardView.java
index 76056ee..7fe5e49 100644
--- a/src/com/android/tv/menu/ChannelCardView.java
+++ b/src/com/android/tv/menu/ChannelCardView.java
@@ -26,12 +26,14 @@
 import android.widget.ImageView;
 import android.widget.ProgressBar;
 import android.widget.TextView;
+
 import com.android.tv.MainActivity;
 import com.android.tv.R;
-import com.android.tv.data.Program;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 import com.android.tv.parental.ParentalControlSettings;
 import com.android.tv.util.images.ImageLoader;
+
 import java.util.Objects;
 
 /** A view to render channel card. */
diff --git a/src/com/android/tv/menu/ChannelsPosterPrefetcher.java b/src/com/android/tv/menu/ChannelsPosterPrefetcher.java
index 9cecb9c..3a50230 100644
--- a/src/com/android/tv/menu/ChannelsPosterPrefetcher.java
+++ b/src/com/android/tv/menu/ChannelsPosterPrefetcher.java
@@ -23,13 +23,15 @@
 import android.support.annotation.MainThread;
 import android.support.annotation.NonNull;
 import android.util.Log;
+
 import com.android.tv.R;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.WeakHandler;
 import com.android.tv.data.ChannelImpl;
-import com.android.tv.data.Program;
 import com.android.tv.data.ProgramDataManager;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
+
 import java.util.List;
 
 /** A poster image prefetcher to show the program poster art in the Channels row faster. */
diff --git a/src/com/android/tv/menu/ChannelsRow.java b/src/com/android/tv/menu/ChannelsRow.java
index 7d03bf2..dbfc782 100644
--- a/src/com/android/tv/menu/ChannelsRow.java
+++ b/src/com/android/tv/menu/ChannelsRow.java
@@ -73,6 +73,7 @@
             mTvRecommendation = null;
         }
         mChannelsPosterPrefetcher.cancel();
+        mChannelsAdapter.release();
     }
 
     /** Handle the update event of the recent channel. */
diff --git a/src/com/android/tv/menu/ChannelsRowAdapter.java b/src/com/android/tv/menu/ChannelsRowAdapter.java
index 4a9e476..e6b6103 100644
--- a/src/com/android/tv/menu/ChannelsRowAdapter.java
+++ b/src/com/android/tv/menu/ChannelsRowAdapter.java
@@ -47,6 +47,7 @@
     private final int mMaxCount;
     private final int mMinCount;
     private final ChannelChanger mChannelChanger;
+    private final AccessibilityManager mAccessibilityManager;
 
     private boolean mShowChannelUpDown;
 
@@ -66,10 +67,9 @@
         mMaxCount = maxCount;
         setHasStableIds(true);
         mChannelChanger = (ChannelChanger) (context);
-        AccessibilityManager accessibilityManager =
-                context.getSystemService(AccessibilityManager.class);
-        mShowChannelUpDown = accessibilityManager.isEnabled();
-        accessibilityManager.addAccessibilityStateChangeListener(this);
+        mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
+        mShowChannelUpDown = mAccessibilityManager.isEnabled();
+        mAccessibilityManager.addAccessibilityStateChangeListener(this);
     }
 
     @Override
@@ -316,4 +316,10 @@
         mShowChannelUpDown = enabled;
         update();
     }
+
+    @Override
+    public void release() {
+        mAccessibilityManager.removeAccessibilityStateChangeListener(this);
+        super.release();
+    }
 }
diff --git a/src/com/android/tv/menu/ItemListRowView.java b/src/com/android/tv/menu/ItemListRowView.java
index 7042324..cc6d23c 100644
--- a/src/com/android/tv/menu/ItemListRowView.java
+++ b/src/com/android/tv/menu/ItemListRowView.java
@@ -17,9 +17,9 @@
 package com.android.tv.menu;
 
 import android.content.Context;
-import android.support.v17.leanback.widget.HorizontalGridView;
-import android.support.v17.leanback.widget.OnChildSelectedListener;
-import android.support.v7.widget.RecyclerView;
+import androidx.leanback.widget.HorizontalGridView;
+import androidx.leanback.widget.OnChildSelectedListener;
+import androidx.recyclerview.widget.RecyclerView;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.LayoutInflater;
@@ -44,6 +44,8 @@
         void onSelected();
 
         void onDeselected();
+
+        boolean requestFocusWithAccessibility();
     }
 
     private HorizontalGridView mListView;
@@ -114,6 +116,13 @@
         }
     }
 
+    @Override
+    protected void requestChildFocus() {
+        if (mSelectedCard != null) {
+            mSelectedCard.requestFocusWithAccessibility();
+        }
+    }
+
     public abstract static class ItemListAdapter<T>
             extends RecyclerView.Adapter<ItemListAdapter.MyViewHolder> {
         private final MainActivity mMainActivity;
diff --git a/src/com/android/tv/menu/Menu.java b/src/com/android/tv/menu/Menu.java
index 6bdbf87..0687441 100644
--- a/src/com/android/tv/menu/Menu.java
+++ b/src/com/android/tv/menu/Menu.java
@@ -23,7 +23,7 @@
 import android.content.res.Resources;
 import android.support.annotation.IntDef;
 import android.support.annotation.VisibleForTesting;
-import android.support.v17.leanback.widget.HorizontalGridView;
+import androidx.leanback.widget.HorizontalGridView;
 import android.util.Log;
 import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
 import com.android.tv.ChannelTuner;
diff --git a/src/com/android/tv/menu/MenuLayoutManager.java b/src/com/android/tv/menu/MenuLayoutManager.java
index a600f70..8f95db7 100644
--- a/src/com/android/tv/menu/MenuLayoutManager.java
+++ b/src/com/android/tv/menu/MenuLayoutManager.java
@@ -25,15 +25,15 @@
 import android.content.res.Resources;
 import android.graphics.Rect;
 import android.support.annotation.UiThread;
-import android.support.v4.view.animation.FastOutLinearInInterpolator;
-import android.support.v4.view.animation.FastOutSlowInInterpolator;
-import android.support.v4.view.animation.LinearOutSlowInInterpolator;
-import android.support.v7.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView;
 import android.util.Log;
 import android.util.Property;
 import android.view.View;
 import android.view.ViewGroup.MarginLayoutParams;
 import android.widget.TextView;
+import androidx.interpolator.view.animation.FastOutLinearInInterpolator;
+import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
+import androidx.interpolator.view.animation.LinearOutSlowInInterpolator;
 import com.android.tv.R;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.util.Utils;
diff --git a/src/com/android/tv/menu/MenuRow.java b/src/com/android/tv/menu/MenuRow.java
index 8dc12ba..0945a0c 100644
--- a/src/com/android/tv/menu/MenuRow.java
+++ b/src/com/android/tv/menu/MenuRow.java
@@ -31,6 +31,8 @@
 
     private MenuRowView mMenuRowView;
 
+    private boolean mIsReselected = false;
+
     // TODO: Check if the heightResId is really necessary.
     public MenuRow(Context context, Menu menu, int titleResId, int heightResId) {
         this(context, menu, context.getString(titleResId), heightResId);
@@ -100,4 +102,19 @@
     public boolean hideTitleWhenSelected() {
         return false;
     }
+
+    /**
+     * Sets if menu row is reselected.
+     *
+     * @param isReselected {@code true} if row is reselected;
+     * else {@code false}.
+     */
+    public void setIsReselected(boolean isReselected) {
+        mIsReselected = isReselected;
+    }
+
+    /** Returns true if row is reselected. */
+    public boolean isReselected() {
+        return mIsReselected;
+    }
 }
diff --git a/src/com/android/tv/menu/MenuRowFactory.java b/src/com/android/tv/menu/MenuRowFactory.java
index 048d725..a3837a1 100644
--- a/src/com/android/tv/menu/MenuRowFactory.java
+++ b/src/com/android/tv/menu/MenuRowFactory.java
@@ -24,6 +24,7 @@
 import com.android.tv.common.customization.CustomAction;
 import com.android.tv.common.customization.CustomizationManager;
 import com.android.tv.ui.TunableTvView;
+import com.android.tv.common.flags.LegacyFlags;
 import java.util.List;
 
 /** A factory class to create menu rows. */
@@ -31,12 +32,15 @@
     private final MainActivity mMainActivity;
     private final TunableTvView mTvView;
     private final CustomizationManager mCustomizationManager;
+    private final LegacyFlags mLegacyFlags;
 
     /** A constructor. */
-    public MenuRowFactory(MainActivity mainActivity, TunableTvView tvView) {
+    public MenuRowFactory(
+            MainActivity mainActivity, TunableTvView tvView, LegacyFlags mLegacyFlags) {
         mMainActivity = mainActivity;
         mTvView = tvView;
         mCustomizationManager = new CustomizationManager(mainActivity);
+        this.mLegacyFlags = mLegacyFlags;
         mCustomizationManager.initialize();
     }
 
@@ -60,7 +64,8 @@
             return new TvOptionsRow(
                     mMainActivity,
                     menu,
-                    mCustomizationManager.getCustomActions(CustomizationManager.ID_OPTIONS_ROW));
+                    mCustomizationManager.getCustomActions(CustomizationManager.ID_OPTIONS_ROW),
+                    mLegacyFlags);
         }
         return null;
     }
@@ -70,13 +75,17 @@
         /** The ID of the row. */
         public static final String ID = TvOptionsRow.class.getName();
 
-        private TvOptionsRow(Context context, Menu menu, List<CustomAction> customActions) {
+        private TvOptionsRow(
+                Context context,
+                Menu menu,
+                List<CustomAction> customActions,
+                LegacyFlags legacyFlags) {
             super(
                     context,
                     menu,
                     R.string.menu_title_options,
                     R.dimen.action_card_height,
-                    new TvOptionsRowAdapter(context, customActions));
+                    new TvOptionsRowAdapter(context, customActions, legacyFlags));
         }
     }
 
diff --git a/src/com/android/tv/menu/MenuRowView.java b/src/com/android/tv/menu/MenuRowView.java
index a064f35..e09a4ef 100644
--- a/src/com/android/tv/menu/MenuRowView.java
+++ b/src/com/android/tv/menu/MenuRowView.java
@@ -25,6 +25,7 @@
 import android.util.TypedValue;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 import com.android.tv.R;
@@ -89,6 +90,18 @@
         float textSizeDeselected =
                 res.getDimensionPixelSize(R.dimen.menu_row_title_text_size_deselected);
         mTitleViewScaleSelected = textSizeSelected / textSizeDeselected;
+        this.setAccessibilityDelegate(
+                new AccessibilityDelegate() {
+                    @Override
+                    public void sendAccessibilityEvent(View host, int eventType) {
+                        super.sendAccessibilityEvent(host, eventType);
+                        if (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED &&
+                                !mRow.isReselected()) {
+                            requestChildFocus();
+                        }
+                    }
+                }
+        );
     }
 
     @Override
@@ -177,6 +190,9 @@
         mLastFocusView = v;
     }
 
+    /** Subclasses should implement this to request focus on child. */
+    protected abstract void requestChildFocus();
+
     /**
      * Called when the focus of a child view is changed. The inherited class should override this
      * method instead of calling {@link
diff --git a/src/com/android/tv/menu/MenuView.java b/src/com/android/tv/menu/MenuView.java
index f5fec00..add4a77 100644
--- a/src/com/android/tv/menu/MenuView.java
+++ b/src/com/android/tv/menu/MenuView.java
@@ -250,41 +250,42 @@
         // The bounds of the views move and overlap with each other during the animation. In this
         // situation, the framework can't perform the correct focus navigation. So the menu view
         // should search by itself.
-        if (direction == View.FOCUS_UP) {
-            View newView = super.focusSearch(focused, direction);
-            MenuRowView oldfocusedParent = getParentMenuRowView(focused);
-            MenuRowView newFocusedParent = getParentMenuRowView(newView);
-            int selectedPosition = mLayoutManager.getSelectedPosition();
-            if (newFocusedParent != oldfocusedParent) {
-                // The focus leaves from the current menu row view.
-                for (int i = selectedPosition - 1; i >= 0; --i) {
-                    MenuRowView view = mMenuRowViews.get(i);
-                    if (view.getVisibility() == View.VISIBLE) {
-                        return view;
-                    }
-                }
-            }
-            return newView;
-        } else if (direction == View.FOCUS_DOWN) {
-            View newView = super.focusSearch(focused, direction);
-            MenuRowView oldfocusedParent = getParentMenuRowView(focused);
-            MenuRowView newFocusedParent = getParentMenuRowView(newView);
-            int selectedPosition = mLayoutManager.getSelectedPosition();
-            if (newFocusedParent != oldfocusedParent) {
-                // The focus leaves from the current menu row view.
-                int count = mMenuRowViews.size();
-                for (int i = selectedPosition + 1; i < count; ++i) {
-                    MenuRowView view = mMenuRowViews.get(i);
-                    if (view.getVisibility() == View.VISIBLE) {
-                        return view;
-                    }
-                }
-            }
-            return newView;
+        if (direction == View.FOCUS_UP || direction == View.FOCUS_DOWN) {
+            return getUpDownFocus(focused, direction);
         }
         return super.focusSearch(focused, direction);
     }
 
+    private View getUpDownFocus(View focused, int direction) {
+        View newView = super.focusSearch(focused, direction);
+        MenuRowView oldfocusedParent = getParentMenuRowView(focused);
+        MenuRowView newFocusedParent = getParentMenuRowView(newView);
+        int selectedPosition = mLayoutManager.getSelectedPosition();
+        int start, delta;
+        if (direction == View.FOCUS_UP) {
+            start = selectedPosition - 1;
+            delta = -1;
+        } else {
+            start = selectedPosition + 1;
+            delta = 1;
+        }
+        if (newFocusedParent != oldfocusedParent) {
+            // The focus leaves from the current menu row view.
+            int count = mMenuRowViews.size();
+            int i = start;
+            while (i < count && i >= 0) {
+                MenuRowView view = mMenuRowViews.get(i);
+                if (view.getVisibility() == View.VISIBLE) {
+                    mMenuRows.get(i).setIsReselected(false);
+                    return view;
+                }
+                i += delta;
+            }
+        }
+        mMenuRows.get(selectedPosition).setIsReselected(true);
+        return newView;
+    }
+
     private MenuRowView getParentMenuRowView(View view) {
         if (view == null) {
             return null;
diff --git a/src/com/android/tv/menu/PlayControlsButton.java b/src/com/android/tv/menu/PlayControlsButton.java
index ac3292a..1b85d63 100644
--- a/src/com/android/tv/menu/PlayControlsButton.java
+++ b/src/com/android/tv/menu/PlayControlsButton.java
@@ -22,6 +22,7 @@
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.TextView;
@@ -146,4 +147,11 @@
         mIcon.setAlpha(enabled ? ALPHA_ENABLED : ALPHA_DISABLED);
         mLabel.setEnabled(enabled);
     }
+
+    /** Request focus and accessibility focus to the button */
+    public boolean requestFocusWithAccessibility() {
+        return mButton.requestFocus() &&
+                mButton.performAccessibilityAction(
+                        AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
+    }
 }
diff --git a/src/com/android/tv/menu/PlayControlsRowView.java b/src/com/android/tv/menu/PlayControlsRowView.java
index 0ce74ae..5dde3be 100644
--- a/src/com/android/tv/menu/PlayControlsRowView.java
+++ b/src/com/android/tv/menu/PlayControlsRowView.java
@@ -24,6 +24,7 @@
 import android.view.View;
 import android.widget.TextView;
 import android.widget.Toast;
+
 import com.android.tv.MainActivity;
 import com.android.tv.R;
 import com.android.tv.TimeShiftManager;
@@ -31,8 +32,8 @@
 import com.android.tv.TvSingletons;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.data.Program;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 import com.android.tv.dialog.HalfSizedDialogFragment;
 import com.android.tv.dvr.DvrDataManager;
 import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener;
@@ -487,6 +488,11 @@
         }
     }
 
+    @Override
+    protected void requestChildFocus() {
+        mPlayPauseButton.requestFocusWithAccessibility();
+    }
+
     /** Updates the view contents. It is called from the PlayControlsRow. */
     public void update() {
         updateAll(false);
diff --git a/src/com/android/tv/menu/TvOptionsRowAdapter.java b/src/com/android/tv/menu/TvOptionsRowAdapter.java
index fe52b25..418560a 100644
--- a/src/com/android/tv/menu/TvOptionsRowAdapter.java
+++ b/src/com/android/tv/menu/TvOptionsRowAdapter.java
@@ -19,8 +19,8 @@
 import android.content.Context;
 import android.media.tv.TvTrackInfo;
 import com.android.tv.TvOptionsManager;
+import com.android.tv.common.BuildConfig;
 import com.android.tv.common.customization.CustomAction;
-import com.android.tv.common.util.CommonUtils;
 import com.android.tv.data.DisplayMode;
 import com.android.tv.features.TvFeatures;
 import com.android.tv.ui.TvViewUiManager;
@@ -28,6 +28,7 @@
 import com.android.tv.ui.sidepanel.DeveloperOptionFragment;
 import com.android.tv.ui.sidepanel.DisplayModeFragment;
 import com.android.tv.ui.sidepanel.MultiAudioFragment;
+import com.android.tv.common.flags.LegacyFlags;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -35,8 +36,12 @@
  * An adapter of options.
  */
 public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter {
-    public TvOptionsRowAdapter(Context context, List<CustomAction> customActions) {
+    private final LegacyFlags mLegacyFlags;
+
+    public TvOptionsRowAdapter(
+            Context context, List<CustomAction> customActions, LegacyFlags mLegacyFlags) {
         super(context, customActions);
+        this.mLegacyFlags = mLegacyFlags;
     }
 
     @Override
@@ -49,7 +54,7 @@
         }
         actionList.add(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION);
         actionList.add(MenuAction.MORE_CHANNELS_ACTION);
-        if (CommonUtils.isDeveloper()) {
+        if (BuildConfig.ENG || mLegacyFlags.enableDeveloperFeatures()) {
             actionList.add(MenuAction.DEV_ACTION);
         }
         actionList.add(MenuAction.SETTINGS_ACTION);
diff --git a/src/com/android/tv/modules/TvApplicationModule.java b/src/com/android/tv/modules/TvApplicationModule.java
index 45383ae..99753d1 100644
--- a/src/com/android/tv/modules/TvApplicationModule.java
+++ b/src/com/android/tv/modules/TvApplicationModule.java
@@ -16,43 +16,101 @@
 package com.android.tv.modules;
 
 import android.content.Context;
+
 import com.android.tv.MainActivity;
+import com.android.tv.SetupPassthroughActivity;
 import com.android.tv.TvApplication;
+import com.android.tv.common.buildtype.BuildTypeModule;
 import com.android.tv.common.concurrent.NamedThreadFactory;
 import com.android.tv.common.dagger.ApplicationModule;
 import com.android.tv.common.dagger.annotations.ApplicationContext;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.ChannelDataManagerFactory;
+import com.android.tv.data.epg.EpgFetchService;
+import com.android.tv.data.epg.EpgFetcher;
+import com.android.tv.data.epg.EpgFetcherImpl;
+import com.android.tv.dialog.PinDialogFragment;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.DvrDataManagerImpl;
+import com.android.tv.dvr.WritableDvrDataManager;
+import com.android.tv.dvr.ui.playback.DvrPlaybackActivity;
 import com.android.tv.onboarding.OnboardingActivity;
+import com.android.tv.onboarding.SetupSourcesFragment;
+import com.android.tv.setup.SystemSetupActivity;
+import com.android.tv.ui.DetailsActivity;
 import com.android.tv.util.AsyncDbTask;
 import com.android.tv.util.TvInputManagerHelper;
+
+import dagger.Binds;
 import dagger.Module;
 import dagger.Provides;
+import dagger.android.ContributesAndroidInjector;
+
+import com.android.tv.common.flags.LegacyFlags;
+
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
+
 import javax.inject.Singleton;
 
 /** Dagger module for {@link TvApplication}. */
 @Module(
         includes = {
             ApplicationModule.class,
-            TvSingletonsModule.class,
+            BuildTypeModule.class,
+            DetailsActivity.Module.class,
+            DvrPlaybackActivity.Module.class,
             MainActivity.Module.class,
-            OnboardingActivity.Module.class
+            OnboardingActivity.Module.class,
+            SetupPassthroughActivity.Module.class,
+            SetupSourcesFragment.ContentFragment.Module.class,
+            SystemSetupActivity.Module.class,
+            TvSingletonsModule.class,
         })
-public class TvApplicationModule {
+public abstract class TvApplicationModule {
     private static final NamedThreadFactory THREAD_FACTORY = new NamedThreadFactory("tv-app-db");
 
     @Provides
     @AsyncDbTask.DbExecutor
     @Singleton
-    Executor providesDbExecutor() {
+    static Executor providesDbExecutor() {
         return Executors.newSingleThreadExecutor(THREAD_FACTORY);
     }
 
     @Provides
     @Singleton
-    TvInputManagerHelper providesTvInputManagerHelper(@ApplicationContext Context context) {
-        TvInputManagerHelper tvInputManagerHelper = new TvInputManagerHelper(context);
+    static TvInputManagerHelper providesTvInputManagerHelper(
+            @ApplicationContext Context context, LegacyFlags legacyFlags) {
+        TvInputManagerHelper tvInputManagerHelper = new TvInputManagerHelper(context, legacyFlags);
         tvInputManagerHelper.start();
+        // Since this is injected as a Lazy in the application start is delayed.
         return tvInputManagerHelper;
     }
+
+    @Provides
+    @Singleton
+    static ChannelDataManager providesChannelDataManager(ChannelDataManagerFactory factory) {
+        ChannelDataManager channelDataManager = factory.create();
+        channelDataManager.start();
+        // Since this is injected as a Lazy in the application start is delayed.
+        return channelDataManager;
+    }
+
+    @Binds
+    @Singleton
+    abstract DvrDataManager providesDvrDataManager(DvrDataManagerImpl impl);
+
+    @Binds
+    @Singleton
+    abstract WritableDvrDataManager providesWritableDvrDataManager(DvrDataManagerImpl impl);
+
+    @Binds
+    @Singleton
+    abstract EpgFetcher epgFetcher(EpgFetcherImpl impl);
+
+    @ContributesAndroidInjector
+    abstract PinDialogFragment contributesPinDialogFragment();
+
+    @ContributesAndroidInjector
+    abstract EpgFetchService contributesEpgFetchService();
 }
diff --git a/src/com/android/tv/modules/TvSingletonsModule.java b/src/com/android/tv/modules/TvSingletonsModule.java
index f998c08..f8d10fd 100644
--- a/src/com/android/tv/modules/TvSingletonsModule.java
+++ b/src/com/android/tv/modules/TvSingletonsModule.java
@@ -16,8 +16,9 @@
 package com.android.tv.modules;
 
 import com.android.tv.TvSingletons;
-import com.android.tv.data.ChannelDataManager;
 import com.android.tv.data.ProgramDataManager;
+import com.android.tv.dvr.DvrWatchedPositionManager;
+
 import dagger.Module;
 import dagger.Provides;
 
@@ -36,8 +37,8 @@
     }
 
     @Provides
-    ChannelDataManager providesChannelDataManager() {
-        return mTvSingletons.getChannelDataManager();
+    DvrWatchedPositionManager providesDvrWatchedPositionManager() {
+        return mTvSingletons.getDvrWatchedPositionManager();
     }
 
     @Provides
diff --git a/src/com/android/tv/onboarding/OnboardingActivity.java b/src/com/android/tv/onboarding/OnboardingActivity.java
index 776ae66..1739e5a 100644
--- a/src/com/android/tv/onboarding/OnboardingActivity.java
+++ b/src/com/android/tv/onboarding/OnboardingActivity.java
@@ -88,7 +88,7 @@
         TvSingletons singletons = TvSingletons.getSingletons(this);
         mInputManager = singletons.getTvInputManagerHelper();
         if (PermissionUtils.hasAccessAllEpg(this) || PermissionUtils.hasReadTvListings(this)) {
-            // Make the channels of the new inputs which have been setup outside Live TV
+            // Make the channels of the new inputs which have been setup outside TV app
             // browsable.
             if (mChannelDataManager.isDbLoadFinished()) {
                 mSetupUtils.markNewChannelsBrowsable();
@@ -187,7 +187,7 @@
                             }
                             // Even though other app can handle the intent, the setup launched by
                             // Live
-                            // channels should go through Live channels SetupPassthroughActivity.
+                            // channels should go through TV app SetupPassthroughActivity.
                             intent.setComponent(
                                     new ComponentName(this, SetupPassthroughActivity.class));
                             try {
diff --git a/src/com/android/tv/onboarding/SetupSourcesFragment.java b/src/com/android/tv/onboarding/SetupSourcesFragment.java
index 3566c9c..2b0a736 100644
--- a/src/com/android/tv/onboarding/SetupSourcesFragment.java
+++ b/src/com/android/tv/onboarding/SetupSourcesFragment.java
@@ -16,33 +16,44 @@
 
 package com.android.tv.onboarding;
 
-import android.content.Context;
+import android.app.Activity;
 import android.graphics.Typeface;
 import android.media.tv.TvInputInfo;
 import android.media.tv.TvInputManager.TvInputCallback;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
-import android.support.v17.leanback.widget.GuidedActionsStylist;
-import android.support.v17.leanback.widget.VerticalGridView;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.TextView;
+
+import androidx.leanback.widget.GuidanceStylist.Guidance;
+import androidx.leanback.widget.GuidedAction;
+import androidx.leanback.widget.GuidedActionsStylist;
+import androidx.leanback.widget.VerticalGridView;
+
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
 import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
 import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
 import com.android.tv.data.ChannelDataManager;
 import com.android.tv.data.TvInputNewComparator;
+import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
 import com.android.tv.ui.GuidedActionsStylistWithDivider;
 import com.android.tv.util.SetupUtils;
 import com.android.tv.util.TvInputManagerHelper;
+
+import com.google.common.base.Optional;
+
+import dagger.android.AndroidInjection;
+import dagger.android.ContributesAndroidInjector;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
+import javax.inject.Inject;
+
 /** A fragment for channel source info/setup. */
 public class SetupSourcesFragment extends SetupMultiPaneFragment {
     /** The action category for the actions which is fired from this fragment. */
@@ -106,9 +117,10 @@
         private static final int PENDING_ACTION_INPUT_CHANGED = 1;
         private static final int PENDING_ACTION_CHANNEL_CHANGED = 2;
 
-        private TvInputManagerHelper mInputManager;
-        private ChannelDataManager mChannelDataManager;
-        private SetupUtils mSetupUtils;
+        @Inject TvInputManagerHelper mInputManager;
+        @Inject ChannelDataManager mChannelDataManager;
+        @Inject SetupUtils mSetupUtils;
+        @Inject Optional<BuiltInTunerManager> mBuiltInTunerManagerOptional;
         private List<TvInputInfo> mInputs;
         private int mKnownInputStartIndex;
         private int mDoneInputStartIndex;
@@ -187,30 +199,31 @@
 
         @Override
         public void onCreate(Bundle savedInstanceState) {
-            Context context = getActivity();
-            TvSingletons singletons = TvSingletons.getSingletons(context);
-            mInputManager = singletons.getTvInputManagerHelper();
-            mChannelDataManager = singletons.getChannelDataManager();
-            mSetupUtils = singletons.getSetupUtils();
+            super.onCreate(savedInstanceState);
+            mParentFragment = (SetupSourcesFragment) getParentFragment();
+        }
+
+        @Override
+        public void onAttach(Activity activity) {
+            AndroidInjection.inject(this);
+            super.onAttach(activity);
             buildInputs();
             mInputManager.addCallback(mInputCallback);
             mChannelDataManager.addListener(mChannelDataManagerListener);
-            super.onCreate(savedInstanceState);
             mParentFragment = (SetupSourcesFragment) getParentFragment();
-            if (singletons.getBuiltInTunerManager().isPresent()) {
-                singletons
-                        .getBuiltInTunerManager()
+            if (mBuiltInTunerManagerOptional.isPresent()) {
+                mBuiltInTunerManagerOptional
                         .get()
                         .getTunerInputController()
-                        .executeNetworkTunerDiscoveryAsyncTask(getContext());
+                        .executeNetworkTunerDiscoveryAsyncTask(activity);
             }
         }
 
         @Override
-        public void onDestroy() {
-            super.onDestroy();
+        public void onDetach() {
             mChannelDataManager.removeListener(mChannelDataManagerListener);
             mInputManager.removeCallback(mInputCallback);
+            super.onDetach();
         }
 
         @NonNull
@@ -404,5 +417,13 @@
                 setAccessibilityDelegate(vh, action);
             }
         }
+        /**
+         * Exports {@link ContentFragment} for Dagger codegen to create the appropriate injector.
+         */
+        @dagger.Module
+        public abstract static class Module {
+            @ContributesAndroidInjector
+            abstract ContentFragment contributesContentFragment();
+        }
     }
 }
diff --git a/src/com/android/tv/onboarding/WelcomeFragment.java b/src/com/android/tv/onboarding/WelcomeFragment.java
index 8c119a8..667da05 100644
--- a/src/com/android/tv/onboarding/WelcomeFragment.java
+++ b/src/com/android/tv/onboarding/WelcomeFragment.java
@@ -25,7 +25,7 @@
 import android.content.Context;
 import android.os.Bundle;
 import android.support.annotation.Nullable;
-import android.support.v17.leanback.app.OnboardingFragment;
+import androidx.leanback.app.OnboardingFragment;
 import android.text.Editable;
 import android.text.TextWatcher;
 import android.view.Gravity;
@@ -621,9 +621,9 @@
             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
         View view = super.onCreateView(inflater, container, savedInstanceState);
         setLogoResourceId(R.drawable.splash_logo);
-        mTitleView = view.findViewById(android.support.v17.leanback.R.id.title);
-        mPagingIndicator = view.findViewById(android.support.v17.leanback.R.id.page_indicator);
-        mStartButton = view.findViewById(android.support.v17.leanback.R.id.button_start);
+        mTitleView = view.findViewById(androidx.leanback.R.id.title);
+        mPagingIndicator = view.findViewById(androidx.leanback.R.id.page_indicator);
+        mStartButton = view.findViewById(androidx.leanback.R.id.button_start);
 
         mStartButton.setAccessibilityDelegate(
                 new AccessibilityDelegate() {
diff --git a/src/com/android/tv/parental/ContentRatingsManager.java b/src/com/android/tv/parental/ContentRatingsManager.java
index 32a1325..174039b 100644
--- a/src/com/android/tv/parental/ContentRatingsManager.java
+++ b/src/com/android/tv/parental/ContentRatingsManager.java
@@ -22,6 +22,7 @@
 import android.support.annotation.Nullable;
 import android.text.TextUtils;
 import com.android.tv.R;
+import com.android.tv.common.util.PermissionUtils;
 import com.android.tv.parental.ContentRatingSystem.Rating;
 import com.android.tv.parental.ContentRatingSystem.SubRating;
 import com.android.tv.util.TvInputManagerHelper;
@@ -42,13 +43,14 @@
 
     public void update() {
         mContentRatingSystems.clear();
-        ContentRatingsParser parser = new ContentRatingsParser(mContext);
-
-        List<TvContentRatingSystemInfo> infos = mTvInputManager.getTvContentRatingSystemList();
-        for (TvContentRatingSystemInfo info : infos) {
-            List<ContentRatingSystem> list = parser.parse(info);
-            if (list != null) {
-                mContentRatingSystems.addAll(list);
+        if (PermissionUtils.hasReadContetnRatingSystem(mContext)) {
+            ContentRatingsParser parser = new ContentRatingsParser(mContext);
+            List<TvContentRatingSystemInfo> infos = mTvInputManager.getTvContentRatingSystemList();
+            for (TvContentRatingSystemInfo info : infos) {
+                List<ContentRatingSystem> list = parser.parse(info);
+                if (list != null) {
+                    mContentRatingSystems.addAll(list);
+                }
             }
         }
     }
diff --git a/src/com/android/tv/parental/ParentalControlSettings.java b/src/com/android/tv/parental/ParentalControlSettings.java
index b41b160..9990ae3 100644
--- a/src/com/android/tv/parental/ParentalControlSettings.java
+++ b/src/com/android/tv/parental/ParentalControlSettings.java
@@ -19,12 +19,12 @@
 import android.content.Context;
 import android.media.tv.TvContentRating;
 import android.media.tv.TvInputManager;
-import com.android.tv.common.experiments.Experiments;
 import com.android.tv.parental.ContentRatingSystem.Rating;
 import com.android.tv.parental.ContentRatingSystem.SubRating;
 import com.android.tv.util.TvSettings;
 import com.android.tv.util.TvSettings.ContentRatingLevel;
 import com.google.common.collect.ImmutableList;
+import com.android.tv.common.flags.LegacyFlags;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -40,14 +40,16 @@
 
     private final Context mContext;
     private final TvInputManager mTvInputManager;
+    private final LegacyFlags mLegacyFlags;
 
     // mRatings is expected to be synchronized with mTvInputManager.getBlockedRatings().
     private Set<TvContentRating> mRatings;
     private Set<TvContentRating> mCustomRatings;
 
-    public ParentalControlSettings(Context context) {
+    public ParentalControlSettings(Context context, LegacyFlags legacyFlags) {
         mContext = context;
         mTvInputManager = (TvInputManager) mContext.getSystemService(Context.TV_INPUT_SERVICE);
+        mLegacyFlags = legacyFlags;
     }
 
     public boolean isParentalControlsEnabled() {
@@ -130,7 +132,7 @@
         } else {
             mRatings = ContentRatingLevelPolicy.getRatingsForLevel(this, manager, level);
             if (level != TvSettings.CONTENT_RATING_LEVEL_NONE
-                    && Boolean.TRUE.equals(Experiments.ENABLE_UNRATED_CONTENT_SETTINGS.get())) {
+                    && mLegacyFlags.enableUnratedContentSettings()) {
                 // UNRATED contents should be blocked unless the rating level is none or custom
                 mRatings.add(TvContentRating.UNRATED);
             }
diff --git a/src/com/android/tv/perf/PerformanceMonitor.java b/src/com/android/tv/perf/PerformanceMonitor.java
index b1ae759..30197c7 100644
--- a/src/com/android/tv/perf/PerformanceMonitor.java
+++ b/src/com/android/tv/perf/PerformanceMonitor.java
@@ -96,4 +96,14 @@
      * @return true if the activity is available to start
      */
     boolean startPerformanceMonitorEventDebugActivity(Context context);
+
+    /**
+     * Initialize crash monitoring for an app by wrapping the default {@link
+     * Thread.UncaughtExceptionHandler} with a handler that can report crashes to the performance
+     * montitor and then delegate the handling of the UncaughtException to the original default
+     * {@link Thread.UncaughtExceptionHandler}.
+     *
+     * <p>Note: This will override the current {@link Thread.UncaughtExceptionHandler}.
+     */
+    void startCrashMonitor();
 }
diff --git a/src/com/android/tv/perf/PerformanceMonitorManager.java b/src/com/android/tv/perf/PerformanceMonitorManager.java
deleted file mode 100644
index db6667d..0000000
--- a/src/com/android/tv/perf/PerformanceMonitorManager.java
+++ /dev/null
@@ -1,38 +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.perf;
-
-import android.app.Application;
-
-/** Manages the initialization of Performance Monitoring. */
-public interface PerformanceMonitorManager {
-
-    /**
-     * Initializes the {@link com.android.tv.perf.PerformanceMonitor}.
-     *
-     * <p>This should only be called once.
-     */
-    PerformanceMonitor initialize(Application app);
-
-    /**
-     * Returns a lightweight object to help measure both cold and warm startup latency.
-     *
-     * <p>This method is idempotent and lightweight. It can be called multiple times and does not
-     * need to be cached.
-     */
-    StartupMeasure getStartupMeasure();
-}
diff --git a/src/com/android/tv/perf/PerformanceMonitorManagerFactory.java b/src/com/android/tv/perf/PerformanceMonitorManagerFactory.java
deleted file mode 100644
index fe3ea14..0000000
--- a/src/com/android/tv/perf/PerformanceMonitorManagerFactory.java
+++ /dev/null
@@ -1,35 +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.perf;
-
-import com.android.tv.perf.stub.StubPerformanceMonitorManager;
-import javax.inject.Inject;
-
-public final class PerformanceMonitorManagerFactory {
-    private static final PerformanceMonitorManagerFactory INSTANCE =
-            new PerformanceMonitorManagerFactory();
-
-    @Inject
-    public PerformanceMonitorManagerFactory() {}
-
-    public static PerformanceMonitorManager create() {
-        return INSTANCE.get();
-    }
-
-    public PerformanceMonitorManager get() {
-        return new StubPerformanceMonitorManager();
-    }
-}
diff --git a/src/com/android/tv/perf/StartupMeasure.java b/src/com/android/tv/perf/StartupMeasure.java
index 5cf183c..c7fa50f 100644
--- a/src/com/android/tv/perf/StartupMeasure.java
+++ b/src/com/android/tv/perf/StartupMeasure.java
@@ -19,8 +19,16 @@
 import android.app.Application;
 
 /**
- * Measures App startup. This interface is lightweight to help measure both cold and warm startup
- * latency. Implementations must not throw any Exception.
+ * Measures App startup.
+ *
+ * <p>This interface is lightweight to help measure both cold and warm startup latency.
+ * Implementations must not throw any Exception.
+ *
+ * <p>Because this class needs to be used in static initialization blocks, it can not be injected
+ * via dagger.
+ *
+ * <p>Creating implementations of this interface must be idempotent and lightweight. It does not
+ * need to be cached.
  */
 public interface StartupMeasure {
 
diff --git a/src/com/android/tv/perf/StartupMeasureFactory.java b/src/com/android/tv/perf/StartupMeasureFactory.java
new file mode 100644
index 0000000..a99c88a
--- /dev/null
+++ b/src/com/android/tv/perf/StartupMeasureFactory.java
@@ -0,0 +1,43 @@
+/*
+ * 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.perf;
+
+
+import com.android.tv.perf.stub.StubStartupMeasure;
+
+import com.google.common.base.Supplier;
+import java.lang.Override;
+import javax.inject.Inject;
+
+/** Factory for {@link StartupMeasure}.
+ *
+ * <p>Hardcoded to {@link StubStartupMeasure}.
+ */
+public final class StartupMeasureFactory implements Supplier<StartupMeasure> {
+    private static final StartupMeasureFactory INSTANCE = new StartupMeasureFactory();
+
+    @Inject
+    public StartupMeasureFactory() {}
+
+    public static StartupMeasure create() {
+        return INSTANCE.get();
+    }
+
+    @Override
+    public StartupMeasure get() {
+        return new StubStartupMeasure();
+    }
+}
diff --git a/src/com/android/tv/perf/stub/StubPerformanceMonitor.java b/src/com/android/tv/perf/stub/StubPerformanceMonitor.java
index 80c2f6c..ac3dc25 100644
--- a/src/com/android/tv/perf/stub/StubPerformanceMonitor.java
+++ b/src/com/android/tv/perf/stub/StubPerformanceMonitor.java
@@ -56,7 +56,6 @@
         return false;
     }
 
-    public static TimerEvent startBootstrapTimer() {
-        return new TimerEvent() {};
-    }
+    @Override
+    public void startCrashMonitor() {}
 }
diff --git a/src/com/android/tv/perf/stub/StubPerformanceMonitorManager.java b/src/com/android/tv/perf/stub/StubPerformanceMonitorManager.java
deleted file mode 100644
index 0c26815..0000000
--- a/src/com/android/tv/perf/stub/StubPerformanceMonitorManager.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tv.perf.stub;
-
-import android.app.Application;
-import com.android.tv.perf.PerformanceMonitor;
-import com.android.tv.perf.PerformanceMonitorManager;
-import com.android.tv.perf.StartupMeasure;
-
-/** Manages a stub implementation of Performance Monitoring. */
-public class StubPerformanceMonitorManager implements PerformanceMonitorManager {
-
-    @Override
-    public PerformanceMonitor initialize(Application app) {
-        return new StubPerformanceMonitor();
-    }
-
-    @Override
-    public StartupMeasure getStartupMeasure() {
-        return new StubStartupMeasure();
-    }
-}
diff --git a/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java b/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java
index 3fb6624..5fa7606 100644
--- a/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java
+++ b/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java
@@ -42,7 +42,7 @@
     // AC3 capabilities stat is sent to Google Analytics just once in order to avoid
     // duplicated stat reports since it doesn't change over time in most cases.
     // Increase this revision when we should force the stat to be sent again.
-    // TODO: Consier using custom metrics.
+    // TODO: Consider using custom metrics.
     private static final int REPORT_REVISION = 1;
 
     private final Context mContext;
diff --git a/src/com/android/tv/receiver/PackageIntentsReceiver.java b/src/com/android/tv/receiver/PackageIntentsReceiver.java
index 5bc6d72..7ff67b5 100644
--- a/src/com/android/tv/receiver/PackageIntentsReceiver.java
+++ b/src/com/android/tv/receiver/PackageIntentsReceiver.java
@@ -23,9 +23,7 @@
 import android.util.Log;
 import com.android.tv.Starter;
 import com.android.tv.TvSingletons;
-import com.android.tv.features.TvFeatures;
 import com.android.tv.util.Partner;
-import com.google.android.tv.partner.support.EpgContract;
 
 /** A class for handling the broadcast intents from PackageManager. */
 public class PackageIntentsReceiver extends BroadcastReceiver {
diff --git a/src/com/android/tv/recommendation/ChannelPreviewUpdater.java b/src/com/android/tv/recommendation/ChannelPreviewUpdater.java
index 2590a33..61ebb2d 100644
--- a/src/com/android/tv/recommendation/ChannelPreviewUpdater.java
+++ b/src/com/android/tv/recommendation/ChannelPreviewUpdater.java
@@ -27,15 +27,18 @@
 import android.support.annotation.RequiresApi;
 import android.text.TextUtils;
 import android.util.Log;
+
 import androidx.tvprovider.media.tv.TvContractCompat;
+
 import com.android.tv.Starter;
 import com.android.tv.TvSingletons;
 import com.android.tv.data.PreviewDataManager;
 import com.android.tv.data.PreviewProgramContent;
-import com.android.tv.data.Program;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 import com.android.tv.parental.ParentalControlSettings;
 import com.android.tv.util.Utils;
+
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
diff --git a/src/com/android/tv/recommendation/ChannelRecord.java b/src/com/android/tv/recommendation/ChannelRecord.java
index c7a7cb3..f047aac 100644
--- a/src/com/android/tv/recommendation/ChannelRecord.java
+++ b/src/com/android/tv/recommendation/ChannelRecord.java
@@ -19,10 +19,12 @@
 import android.content.Context;
 import android.support.annotation.GuardedBy;
 import android.support.annotation.VisibleForTesting;
+
 import com.android.tv.TvSingletons;
-import com.android.tv.data.Program;
 import com.android.tv.data.ProgramDataManager;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
+
 import java.util.ArrayDeque;
 import java.util.Deque;
 
diff --git a/src/com/android/tv/recommendation/NotificationService.java b/src/com/android/tv/recommendation/NotificationService.java
index f40a086..1652bd7 100644
--- a/src/com/android/tv/recommendation/NotificationService.java
+++ b/src/com/android/tv/recommendation/NotificationService.java
@@ -40,19 +40,21 @@
 import android.util.Log;
 import android.util.SparseLongArray;
 import android.view.View;
+
 import com.android.tv.MainActivityWrapper.OnCurrentChannelChangeListener;
 import com.android.tv.R;
 import com.android.tv.Starter;
 import com.android.tv.TvSingletons;
 import com.android.tv.common.CommonConstants;
 import com.android.tv.common.WeakHandler;
-import com.android.tv.data.Program;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 import com.android.tv.util.TvInputManagerHelper;
 import com.android.tv.util.Utils;
 import com.android.tv.util.images.BitmapUtils;
 import com.android.tv.util.images.BitmapUtils.ScaledBitmapInfo;
 import com.android.tv.util.images.ImageLoader;
+
 import java.util.ArrayList;
 import java.util.List;
 
diff --git a/src/com/android/tv/recommendation/RecommendationDataManager.java b/src/com/android/tv/recommendation/RecommendationDataManager.java
index fc20031..e254ba5 100644
--- a/src/com/android/tv/recommendation/RecommendationDataManager.java
+++ b/src/com/android/tv/recommendation/RecommendationDataManager.java
@@ -34,14 +34,17 @@
 import android.support.annotation.Nullable;
 import android.support.annotation.WorkerThread;
 import android.util.Log;
+
 import com.android.tv.TvSingletons;
 import com.android.tv.common.WeakHandler;
 import com.android.tv.common.util.PermissionUtils;
 import com.android.tv.data.ChannelDataManager;
-import com.android.tv.data.Program;
+import com.android.tv.data.ProgramImpl;
 import com.android.tv.data.WatchedHistoryManager;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 import com.android.tv.util.TvUriMatcher;
+
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -232,6 +235,9 @@
 
     @MainThread
     private void stop() {
+        if (mWatchedHistoryManager != null) {
+            mWatchedHistoryManager.setListener(null);
+        }
         for (int what = MSG_FIRST; what <= MSG_LAST; ++what) {
             mHandler.removeMessages(what);
         }
@@ -359,7 +365,7 @@
             WatchedHistoryManager.WatchedRecord watchedRecord) {
         long endTime = watchedRecord.watchedStartTime + watchedRecord.duration;
         Program program =
-                new Program.Builder()
+                new ProgramImpl.Builder()
                         .setChannelId(watchedRecord.channelId)
                         .setTitle("")
                         .setStartTimeUtcMillis(watchedRecord.watchedStartTime)
@@ -411,7 +417,7 @@
         }
 
         Program program =
-                new Program.Builder()
+                new ProgramImpl.Builder()
                         .setChannelId(cursor.getLong(mIndexWatchChannelId))
                         .setTitle(cursor.getString(mIndexProgramTitle))
                         .setStartTimeUtcMillis(cursor.getLong(mIndexProgramStartTime))
diff --git a/src/com/android/tv/recommendation/Recommender.java b/src/com/android/tv/recommendation/Recommender.java
index f350799..a8535a4 100644
--- a/src/com/android/tv/recommendation/Recommender.java
+++ b/src/com/android/tv/recommendation/Recommender.java
@@ -20,7 +20,9 @@
 import android.support.annotation.VisibleForTesting;
 import android.util.Log;
 import android.util.Pair;
+
 import com.android.tv.data.api.Channel;
+
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -127,7 +129,7 @@
                 }
             }
             if (!mIncludeRecommendedOnly || maxScore != Evaluator.NOT_RECOMMENDED) {
-                records.add(new Pair<>(cr.getChannel(), maxScore));
+                records.add(Pair.create(cr.getChannel(), maxScore));
             }
         }
         if (size > records.size()) {
diff --git a/src/com/android/tv/recommendation/RoutineWatchEvaluator.java b/src/com/android/tv/recommendation/RoutineWatchEvaluator.java
index 9240682..b3952c0 100644
--- a/src/com/android/tv/recommendation/RoutineWatchEvaluator.java
+++ b/src/com/android/tv/recommendation/RoutineWatchEvaluator.java
@@ -19,7 +19,9 @@
 import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
 import android.text.TextUtils;
-import com.android.tv.data.Program;
+
+import com.android.tv.data.api.Program;
+
 import java.text.BreakIterator;
 import java.util.ArrayList;
 import java.util.Calendar;
diff --git a/src/com/android/tv/recommendation/WatchedProgram.java b/src/com/android/tv/recommendation/WatchedProgram.java
index 239de1f..0da9c62 100644
--- a/src/com/android/tv/recommendation/WatchedProgram.java
+++ b/src/com/android/tv/recommendation/WatchedProgram.java
@@ -16,7 +16,7 @@
 
 package com.android.tv.recommendation;
 
-import com.android.tv.data.Program;
+import com.android.tv.data.api.Program;
 
 public final class WatchedProgram {
     private final Program mProgram;
diff --git a/src/com/android/tv/search/DataManagerSearch.java b/src/com/android/tv/search/DataManagerSearch.java
index a649c0a..1a0ada3 100644
--- a/src/com/android/tv/search/DataManagerSearch.java
+++ b/src/com/android/tv/search/DataManagerSearch.java
@@ -26,15 +26,18 @@
 import android.support.annotation.MainThread;
 import android.text.TextUtils;
 import android.util.Log;
+
 import com.android.tv.TvSingletons;
 import com.android.tv.data.ChannelDataManager;
-import com.android.tv.data.Program;
 import com.android.tv.data.ProgramDataManager;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 import com.android.tv.search.LocalSearchProvider.SearchResult;
 import com.android.tv.util.MainThreadExecutor;
 import com.android.tv.util.Utils;
+
 import com.google.common.collect.ImmutableList;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
diff --git a/src/com/android/tv/search/LocalSearchProvider.java b/src/com/android/tv/search/LocalSearchProvider.java
index 5652c98..8699956 100644
--- a/src/com/android/tv/search/LocalSearchProvider.java
+++ b/src/com/android/tv/search/LocalSearchProvider.java
@@ -17,7 +17,6 @@
 package com.android.tv.search;
 
 import android.app.SearchManager;
-import android.content.ContentProvider;
 import android.content.ContentValues;
 import android.database.Cursor;
 import android.database.MatrixCursor;
@@ -28,21 +27,30 @@
 import android.support.annotation.VisibleForTesting;
 import android.text.TextUtils;
 import android.util.Log;
-import com.android.tv.TvSingletons;
+
 import com.android.tv.common.CommonConstants;
 import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.dagger.init.SafePreDaggerInitializer;
 import com.android.tv.common.util.CommonUtils;
 import com.android.tv.common.util.PermissionUtils;
 import com.android.tv.perf.EventNames;
 import com.android.tv.perf.PerformanceMonitor;
 import com.android.tv.perf.TimerEvent;
 import com.android.tv.util.TvUriMatcher;
+
 import com.google.auto.value.AutoValue;
+
+import dagger.android.ContributesAndroidInjector;
+import dagger.android.DaggerContentProvider;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
-public class LocalSearchProvider extends ContentProvider {
+import javax.inject.Inject;
+
+/** Content provider for local search */
+public class LocalSearchProvider extends DaggerContentProvider {
     private static final String TAG = "LocalSearchProvider";
     private static final boolean DEBUG = false;
 
@@ -79,14 +87,18 @@
     private static final String NO_LIVE_CONTENTS = "0";
     private static final String LIVE_CONTENTS = "1";
 
-    private PerformanceMonitor mPerformanceMonitor;
+    @Inject PerformanceMonitor mPerformanceMonitor;
 
     /** Used only for testing */
     private SearchInterface mSearchInterface;
 
     @Override
     public boolean onCreate() {
-        mPerformanceMonitor = TvSingletons.getSingletons(getContext()).getPerformanceMonitor();
+        SafePreDaggerInitializer.init(getContext());
+        if (!super.onCreate()) {
+            Log.e(TAG, "LocalSearchProvider.onCreate() failed.");
+            return false;
+        }
         return true;
     }
 
@@ -221,6 +233,13 @@
         throw new UnsupportedOperationException();
     }
 
+    /** Module for {@link LocalSearchProvider} */
+    @dagger.Module
+    public abstract static class Module {
+        @ContributesAndroidInjector
+        abstract LocalSearchProvider contributesLocalSearchProviderInjector();
+    }
+
     /** A placeholder to a search result. */
     @AutoValue
     public abstract static class SearchResult {
@@ -235,6 +254,8 @@
                     .setProgressPercentage(0);
         }
 
+        public abstract Builder toBuilder();
+
         @AutoValue.Builder
         abstract static class Builder {
             abstract Builder setChannelId(long value);
diff --git a/src/com/android/tv/search/ProgramGuideSearchFragment.java b/src/com/android/tv/search/ProgramGuideSearchFragment.java
index 6c94bd3..fa2e451 100644
--- a/src/com/android/tv/search/ProgramGuideSearchFragment.java
+++ b/src/com/android/tv/search/ProgramGuideSearchFragment.java
@@ -21,18 +21,18 @@
 import android.graphics.drawable.BitmapDrawable;
 import android.os.AsyncTask;
 import android.os.Bundle;
-import android.support.v17.leanback.app.SearchFragment;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.HeaderItem;
-import android.support.v17.leanback.widget.ImageCardView;
-import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.ListRowPresenter;
-import android.support.v17.leanback.widget.ObjectAdapter;
-import android.support.v17.leanback.widget.OnItemViewClickedListener;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.Row;
-import android.support.v17.leanback.widget.RowPresenter;
-import android.support.v17.leanback.widget.SearchBar;
+import androidx.leanback.app.SearchFragment;
+import androidx.leanback.widget.ArrayObjectAdapter;
+import androidx.leanback.widget.HeaderItem;
+import androidx.leanback.widget.ImageCardView;
+import androidx.leanback.widget.ListRow;
+import androidx.leanback.widget.ListRowPresenter;
+import androidx.leanback.widget.ObjectAdapter;
+import androidx.leanback.widget.OnItemViewClickedListener;
+import androidx.leanback.widget.Presenter;
+import androidx.leanback.widget.Row;
+import androidx.leanback.widget.RowPresenter;
+import androidx.leanback.widget.SearchBar;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.LayoutInflater;
diff --git a/src/com/android/tv/search/TvProviderSearch.java b/src/com/android/tv/search/TvProviderSearch.java
index 8a1f51f..c46938a 100644
--- a/src/com/android/tv/search/TvProviderSearch.java
+++ b/src/com/android/tv/search/TvProviderSearch.java
@@ -308,7 +308,7 @@
             if (c != null && c.moveToNext() && !isRatingBlocked(c.getString(2))) {
                 String channelName = result.getTitle();
                 String channelNumber = result.getChannelNumber();
-                SearchResult.Builder builder = SearchResult.builder();
+                SearchResult.Builder builder = result.toBuilder();
                 long startUtcMillis = c.getLong(5);
                 long endUtcMillis = c.getLong(6);
                 builder.setTitle(c.getString(0));
diff --git a/src/com/android/tv/setup/SystemSetupActivity.java b/src/com/android/tv/setup/SystemSetupActivity.java
index b2160b3..999b157 100644
--- a/src/com/android/tv/setup/SystemSetupActivity.java
+++ b/src/com/android/tv/setup/SystemSetupActivity.java
@@ -24,9 +24,9 @@
 import android.media.tv.TvInputInfo;
 import android.os.Bundle;
 import android.widget.Toast;
+
 import com.android.tv.R;
 import com.android.tv.SetupPassthroughActivity;
-import com.android.tv.TvSingletons;
 import com.android.tv.common.CommonConstants;
 import com.android.tv.common.ui.setup.SetupActivity;
 import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
@@ -36,6 +36,11 @@
 import com.android.tv.util.SetupUtils;
 import com.android.tv.util.TvInputManagerHelper;
 
+import dagger.android.AndroidInjection;
+import dagger.android.ContributesAndroidInjector;
+
+import javax.inject.Inject;
+
 /** A activity to start input sources setup fragment for initial setup flow. */
 public class SystemSetupActivity extends SetupActivity {
     private static final String SYSTEM_SETUP =
@@ -43,18 +48,17 @@
     private static final int SHOW_RIPPLE_DURATION_MS = 266;
     private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1;
 
-    private TvInputManagerHelper mInputManager;
+    @Inject TvInputManagerHelper mInputManager;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
+        AndroidInjection.inject(this);
         super.onCreate(savedInstanceState);
         Intent intent = getIntent();
         if (!SYSTEM_SETUP.equals(intent.getAction())) {
             finish();
             return;
         }
-        TvSingletons singletons = TvSingletons.getSingletons(this);
-        mInputManager = singletons.getTvInputManagerHelper();
     }
 
     @Override
@@ -92,7 +96,7 @@
                             }
                             // Even though other app can handle the intent, the setup launched by
                             // Live
-                            // channels should go through Live channels SetupPassthroughActivity.
+                            // channels should go through TV app SetupPassthroughActivity.
                             intent.setComponent(
                                     new ComponentName(this, SetupPassthroughActivity.class));
                             try {
@@ -124,4 +128,13 @@
         }
         return false;
     }
+
+    /**
+     * Exports {@link SystemSetupActivity} for Dagger codegen to create the appropriate injector.
+     */
+    @dagger.Module
+    public abstract static class Module {
+        @ContributesAndroidInjector
+        abstract SystemSetupActivity contributeSystemSetupActivity();
+    }
 }
diff --git a/src/com/android/tv/ui/AppLayerTvView.java b/src/com/android/tv/ui/AppLayerTvView.java
index e2b64a1..4c54fb3 100644
--- a/src/com/android/tv/ui/AppLayerTvView.java
+++ b/src/com/android/tv/ui/AppLayerTvView.java
@@ -21,7 +21,6 @@
 import android.view.SurfaceView;
 import android.view.View;
 import com.android.tv.common.compat.TvViewCompat;
-import com.android.tv.common.util.CommonUtils;
 import com.android.tv.common.util.Debug;
 
 /**
@@ -29,9 +28,14 @@
  *
  * <p>Once an app starts using additional window like SubPanel and it gets window focus, the {@link
  * android.media.tv.TvView#setMain()} does not work because its implementation assumes that the app
- * uses only application layer. TODO: remove this class once the TvView.setMain() is revisited.
+ * uses only application layer.
+ *
+ * <p>TODO: remove this class once the TvView.setMain() is revisited.
  */
 public class AppLayerTvView extends TvViewCompat {
+
+    boolean mUseSecureSurface = true;
+
     public AppLayerTvView(Context context) {
         super(context);
     }
@@ -44,6 +48,11 @@
         super(context, attrs, defStyleAttr);
     }
 
+    /** Set the security of children {@link SurfaceView}s to {@code secure} */
+    public void setUseSecureSurface(boolean secure) {
+        mUseSecureSurface = secure;
+    }
+
     @Override
     public boolean hasWindowFocus() {
         return true;
@@ -53,7 +62,7 @@
     public void onViewAdded(View child) {
         if (child instanceof SurfaceView) {
             // Note: See b/29118070 for detail.
-            ((SurfaceView) child).setSecure(!CommonUtils.isDeveloper());
+            ((SurfaceView) child).setSecure(mUseSecureSurface);
         }
         super.onViewAdded(child);
     }
diff --git a/src/com/android/tv/ui/ChannelBannerView.java b/src/com/android/tv/ui/ChannelBannerView.java
index 00ac7e3..dba9ceb 100644
--- a/src/com/android/tv/ui/ChannelBannerView.java
+++ b/src/com/android/tv/ui/ChannelBannerView.java
@@ -46,13 +46,15 @@
 import android.widget.ProgressBar;
 import android.widget.RelativeLayout;
 import android.widget.TextView;
+
 import com.android.tv.R;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.feature.CommonFeatures;
 import com.android.tv.common.singletons.HasSingletons;
-import com.android.tv.data.Program;
+import com.android.tv.data.ProgramImpl;
 import com.android.tv.data.StreamInfo;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 import com.android.tv.dvr.DvrManager;
 import com.android.tv.dvr.data.ScheduledRecording;
 import com.android.tv.parental.ContentRatingsManager;
@@ -64,7 +66,9 @@
 import com.android.tv.util.images.ImageLoader;
 import com.android.tv.util.images.ImageLoader.ImageLoaderCallback;
 import com.android.tv.util.images.ImageLoader.LoadTvInputLogoTask;
+
 import com.google.common.collect.ImmutableList;
+
 import javax.inject.Provider;
 
 /** A view to render channel banner. */
@@ -118,6 +122,7 @@
     private final TvInputManagerHelper mTvInputManagerHelper;
     // TvOverlayManager is always created after ChannelBannerView
     private final Provider<TvOverlayManager> mTvOverlayManager;
+    private final AccessibilityManager mAccessibilityManager;
 
     private View mChannelView;
 
@@ -265,12 +270,12 @@
         mContentRatingsManager = mTvInputManagerHelper.getContentRatingsManager();
 
         mNoProgram =
-                new Program.Builder()
+                new ProgramImpl.Builder()
                         .setTitle(context.getString(R.string.channel_banner_no_title))
                         .setDescription(EMPTY_STRING)
                         .build();
         mLockedChannelProgram =
-                new Program.Builder()
+                new ProgramImpl.Builder()
                         .setTitle(context.getString(R.string.channel_banner_locked_channel_title))
                         .setDescription(EMPTY_STRING)
                         .build();
@@ -278,8 +283,7 @@
             sClosedCaptionMark = context.getString(R.string.closed_caption);
         }
         mAutoHideScheduler = new AutoHideScheduler(context, this::hide);
-        context.getSystemService(AccessibilityManager.class)
-                .addAccessibilityStateChangeListener(mAutoHideScheduler);
+        mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
     }
 
     @Override
@@ -319,6 +323,18 @@
     }
 
     @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mAccessibilityManager.addAccessibilityStateChangeListener(mAutoHideScheduler);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        mAccessibilityManager.removeAccessibilityStateChangeListener(mAutoHideScheduler);
+        super.onDetachedFromWindow();
+    }
+
+    @Override
     public void onEnterAction(boolean fromEmptyScene) {
         resetAnimationEffects();
         if (fromEmptyScene) {
@@ -735,15 +751,23 @@
         } else {
             ImmutableList<TvContentRating> ratings =
                     (program == null) ? null : program.getContentRatings();
-            for (int i = 0; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) {
-                if (ratings == null || ratings.size() <= i) {
-                    mContentRatingsTextViews[i].setVisibility(View.GONE);
-                } else {
-                    mContentRatingsTextViews[i].setText(
-                            mContentRatingsManager.getDisplayNameForRating(ratings.get(i)));
-                    mContentRatingsTextViews[i].setVisibility(View.VISIBLE);
+            int ratingsViewIndex = 0;
+            if (ratings != null) {
+                for (int i = 0; i < ratings.size(); i++) {
+                    if (ratingsViewIndex < DISPLAYED_CONTENT_RATINGS_COUNT
+                            && !TextUtils.isEmpty(
+                                    mContentRatingsManager.getDisplayNameForRating(
+                                            ratings.get(i)))) {
+                        mContentRatingsTextViews[ratingsViewIndex].setText(
+                                mContentRatingsManager.getDisplayNameForRating(ratings.get(i)));
+                        mContentRatingsTextViews[ratingsViewIndex].setVisibility(View.VISIBLE);
+                        ratingsViewIndex++;
+                    }
                 }
             }
+            while (ratingsViewIndex < DISPLAYED_CONTENT_RATINGS_COUNT) {
+                mContentRatingsTextViews[ratingsViewIndex++].setVisibility(View.GONE);
+            }
         }
     }
 
diff --git a/src/com/android/tv/ui/DetailsActivity.java b/src/com/android/tv/ui/DetailsActivity.java
index 80c0f64..92c13f5 100644
--- a/src/com/android/tv/ui/DetailsActivity.java
+++ b/src/com/android/tv/ui/DetailsActivity.java
@@ -16,12 +16,11 @@
 
 package com.android.tv.ui;
 
-import android.app.Activity;
 import android.content.pm.PackageManager;
 import android.os.Bundle;
 import android.os.Parcelable;
 import android.support.annotation.NonNull;
-import android.support.v17.leanback.app.DetailsFragment;
+import androidx.leanback.app.DetailsFragment;
 import android.transition.Transition;
 import android.transition.Transition.TransitionListener;
 import android.util.Log;
@@ -35,9 +34,12 @@
 import com.android.tv.dvr.ui.browse.RecordedProgramDetailsFragment;
 import com.android.tv.dvr.ui.browse.ScheduledRecordingDetailsFragment;
 import com.android.tv.dvr.ui.browse.SeriesRecordingDetailsFragment;
+import dagger.android.ContributesAndroidInjector;
+import dagger.android.DaggerActivity;
 
 /** Activity to show details view. */
-public class DetailsActivity extends Activity implements PinDialogFragment.OnPinCheckedListener {
+public class DetailsActivity extends DaggerActivity
+        implements PinDialogFragment.OnPinCheckedListener {
     private static final String TAG = "DetailsActivity";
 
     private static final long INVALID_RECORD_ID = -1;
@@ -206,4 +208,15 @@
         }
         finish();
     }
+
+    /** Exports {@link DaggerActivity} for Dagger codegen to create the appropriate injector. */
+    @dagger.Module
+    public abstract static class Module {
+        @ContributesAndroidInjector
+        abstract DetailsActivity contributesDetailsActivityInjector();
+
+        @ContributesAndroidInjector
+        abstract CurrentRecordingDetailsFragment
+                contributesCurrentRecordingDetailsFragmentInjector();
+    }
 }
diff --git a/src/com/android/tv/ui/GuidedActionsStylistWithDivider.java b/src/com/android/tv/ui/GuidedActionsStylistWithDivider.java
index 9685b04..3aba5d1 100644
--- a/src/com/android/tv/ui/GuidedActionsStylistWithDivider.java
+++ b/src/com/android/tv/ui/GuidedActionsStylistWithDivider.java
@@ -17,9 +17,9 @@
 package com.android.tv.ui;
 
 import android.content.Context;
-import android.support.v17.leanback.app.GuidedStepFragment;
-import android.support.v17.leanback.widget.GuidedAction;
-import android.support.v17.leanback.widget.GuidedActionsStylist;
+import androidx.leanback.app.GuidedStepFragment;
+import androidx.leanback.widget.GuidedAction;
+import androidx.leanback.widget.GuidedActionsStylist;
 import com.android.tv.R;
 
 /** Extended stylist class used for {@link GuidedStepFragment} with divider support. */
diff --git a/src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java b/src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java
index 9b916af..703dc24 100644
--- a/src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java
+++ b/src/com/android/tv/ui/OnRepeatedKeyInterceptListener.java
@@ -17,7 +17,7 @@
 
 import android.os.Message;
 import android.support.annotation.NonNull;
-import android.support.v17.leanback.widget.VerticalGridView;
+import androidx.leanback.widget.VerticalGridView;
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.View;
diff --git a/src/com/android/tv/ui/ProgramDetailsFragment.java b/src/com/android/tv/ui/ProgramDetailsFragment.java
index 88a7b2c..bfcebd7 100644
--- a/src/com/android/tv/ui/ProgramDetailsFragment.java
+++ b/src/com/android/tv/ui/ProgramDetailsFragment.java
@@ -23,21 +23,23 @@
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.support.annotation.Nullable;
-import android.support.v17.leanback.app.DetailsFragment;
-import android.support.v17.leanback.widget.Action;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.ClassPresenterSelector;
-import android.support.v17.leanback.widget.DetailsOverviewRow;
-import android.support.v17.leanback.widget.DetailsOverviewRowPresenter;
-import android.support.v17.leanback.widget.OnActionClickedListener;
-import android.support.v17.leanback.widget.PresenterSelector;
-import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
-import android.support.v17.leanback.widget.VerticalGridView;
 import android.text.TextUtils;
+
+import androidx.leanback.app.DetailsFragment;
+import androidx.leanback.widget.Action;
+import androidx.leanback.widget.ArrayObjectAdapter;
+import androidx.leanback.widget.ClassPresenterSelector;
+import androidx.leanback.widget.DetailsOverviewRow;
+import androidx.leanback.widget.DetailsOverviewRowPresenter;
+import androidx.leanback.widget.OnActionClickedListener;
+import androidx.leanback.widget.PresenterSelector;
+import androidx.leanback.widget.SparseArrayObjectAdapter;
+import androidx.leanback.widget.VerticalGridView;
+
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
 import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.data.Program;
+import com.android.tv.data.ProgramImpl;
 import com.android.tv.data.api.Channel;
 import com.android.tv.dvr.DvrDataManager;
 import com.android.tv.dvr.DvrManager;
@@ -64,7 +66,7 @@
     protected DetailsViewBackgroundHelper mBackgroundHelper;
     private ArrayObjectAdapter mRowsAdapter;
     private DetailsOverviewRow mDetailsOverview;
-    private Program mProgram;
+    private ProgramImpl mProgram;
     private String mInputId;
     private ScheduledRecording mScheduledRecording;
     private DvrManager mDvrManager;
@@ -137,7 +139,7 @@
      *     the detail activity and fragment will be ended.
      */
     private boolean onLoadDetails(Bundle args) {
-        Program program = args.getParcelable(DetailsActivity.PROGRAM);
+        ProgramImpl program = args.getParcelable(DetailsActivity.PROGRAM);
         long channelId = args.getLong(DetailsActivity.CHANNEL_ID);
         String inputId = args.getString(DetailsActivity.INPUT_ID);
         if (program != null && channelId != Channel.INVALID_ID && !TextUtils.isEmpty(inputId)) {
diff --git a/src/com/android/tv/ui/SelectInputView.java b/src/com/android/tv/ui/SelectInputView.java
index f4949f0..a0cfad3 100644
--- a/src/com/android/tv/ui/SelectInputView.java
+++ b/src/com/android/tv/ui/SelectInputView.java
@@ -22,8 +22,8 @@
 import android.media.tv.TvInputManager;
 import android.media.tv.TvInputManager.TvInputCallback;
 import android.support.annotation.NonNull;
-import android.support.v17.leanback.widget.VerticalGridView;
-import android.support.v7.widget.RecyclerView;
+import androidx.leanback.widget.VerticalGridView;
+import androidx.recyclerview.widget.RecyclerView;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Log;
diff --git a/src/com/android/tv/ui/TunableTvView.java b/src/com/android/tv/ui/TunableTvView.java
index 5ac6bd8..49f7d4c 100644
--- a/src/com/android/tv/ui/TunableTvView.java
+++ b/src/com/android/tv/ui/TunableTvView.java
@@ -54,11 +54,13 @@
 import android.view.accessibility.AccessibilityManager;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
+
 import com.android.tv.InputSessionManager;
 import com.android.tv.InputSessionManager.TvViewSession;
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
 import com.android.tv.analytics.Tracker;
+import com.android.tv.common.BuildConfig;
 import com.android.tv.common.CommonConstants;
 import com.android.tv.common.compat.TvInputConstantCompat;
 import com.android.tv.common.compat.TvViewCompat.TvInputCallbackCompat;
@@ -67,11 +69,11 @@
 import com.android.tv.common.util.Debug;
 import com.android.tv.common.util.DurationTimer;
 import com.android.tv.common.util.PermissionUtils;
-import com.android.tv.data.Program;
 import com.android.tv.data.ProgramDataManager;
 import com.android.tv.data.StreamInfo;
 import com.android.tv.data.WatchedHistoryManager;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 import com.android.tv.features.TvFeatures;
 import com.android.tv.parental.ContentRatingsManager;
 import com.android.tv.parental.ParentalControlSettings;
@@ -81,6 +83,9 @@
 import com.android.tv.util.TvInputManagerHelper;
 import com.android.tv.util.Utils;
 import com.android.tv.util.images.ImageLoader;
+
+import com.android.tv.common.flags.LegacyFlags;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.List;
@@ -317,7 +322,7 @@
                     if (DEBUG) Log.d(TAG, "onVideoAvailable: {inputId=" + inputId + "}");
                     Debug.getTimer(Debug.TAG_START_UP_TIMER)
                             .log(
-                                    "Start up of Live TV ends,"
+                                    "Start up of TV app ends,"
                                             + " TunableTvView.onVideoAvailable resets timer");
                     Debug.getTimer(Debug.TAG_START_UP_TIMER).reset();
                     Debug.removeTimer(Debug.TAG_START_UP_TIMER);
@@ -473,8 +478,12 @@
     }
 
     public void initialize(
-            ProgramDataManager programDataManager, TvInputManagerHelper tvInputManagerHelper) {
+            ProgramDataManager programDataManager,
+            TvInputManagerHelper tvInputManagerHelper,
+            LegacyFlags mLegacyFlags) {
         mTvView = findViewById(R.id.tv_view);
+        mTvView.setUseSecureSurface(!BuildConfig.ENG && !mLegacyFlags.enableDeveloperFeatures());
+
         mProgramDataManager = programDataManager;
         mInputManagerHelper = tvInputManagerHelper;
         mContentRatingsManager = tvInputManagerHelper.getContentRatingsManager();
@@ -553,7 +562,9 @@
     }
 
     public void setMain() {
-        mTvView.setMain();
+        if (PermissionUtils.hasChangeHdmiCecActiveSource(getContext())) {
+            mTvView.setMain();
+        }
     }
 
     public void setWatchedHistoryManager(WatchedHistoryManager watchedHistoryManager) {
@@ -653,17 +664,22 @@
         // To reduce the IPCs, unregister the callback here and register it when necessary.
         mTvView.setTimeShiftPositionCallback(null);
         setTimeShiftAvailable(false);
-        if (needSurfaceSizeUpdate && mFixedSurfaceWidth > 0 && mFixedSurfaceHeight > 0) {
-            // When the input is changed, TvView recreates its SurfaceView internally.
-            // So we need to call SurfaceHolder.setFixedSize for the new SurfaceView.
-            getSurfaceView().getHolder().setFixedSize(mFixedSurfaceWidth, mFixedSurfaceHeight);
-        }
         mVideoUnavailableReason = TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING;
         if (mTvViewSession != null) {
             mTvViewSession.tune(channel, params, listener);
         } else {
             mTvView.tune(mInputInfo.getId(), mCurrentChannel.getUri(), params);
         }
+        if (needSurfaceSizeUpdate && mFixedSurfaceWidth > 0 && mFixedSurfaceHeight > 0) {
+            // When the input is changed, TvView recreates its SurfaceView internally.
+            // So we need to call SurfaceHolder.setFixedSize for the new SurfaceView.
+            SurfaceView surfaceView = getSurfaceView();
+            if (surfaceView != null) {
+                surfaceView.getHolder().setFixedSize(mFixedSurfaceWidth, mFixedSurfaceHeight);
+            } else {
+                Log.w(TAG, "Failed to set fixed size for surface view: Null surface view");
+            }
+        }
         updateBlockScreenAndMuting();
         if (mOnTuneListener != null) {
             mOnTuneListener.onStreamInfoChanged(this, true);
diff --git a/src/com/android/tv/ui/TvOverlayManager.java b/src/com/android/tv/ui/TvOverlayManager.java
index b2854a1..0ab7c68 100644
--- a/src/com/android/tv/ui/TvOverlayManager.java
+++ b/src/com/android/tv/ui/TvOverlayManager.java
@@ -33,6 +33,7 @@
 import android.view.KeyEvent;
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
+
 import com.android.tv.ChannelTuner;
 import com.android.tv.MainActivity;
 import com.android.tv.MainActivity.KeyHandlerResultType;
@@ -47,6 +48,7 @@
 import com.android.tv.common.ui.setup.SetupFragment;
 import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
 import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.ProgramDataManager;
 import com.android.tv.dialog.DvrHistoryDialogFragment;
 import com.android.tv.dialog.FullscreenDialogFragment;
 import com.android.tv.dialog.HalfSizedDialogFragment;
@@ -68,6 +70,12 @@
 import com.android.tv.ui.sidepanel.SideFragmentManager;
 import com.android.tv.ui.sidepanel.parentalcontrols.RatingsFragment;
 import com.android.tv.util.TvInputManagerHelper;
+
+import com.google.auto.factory.AutoFactory;
+import com.google.auto.factory.Provided;
+
+import com.android.tv.common.flags.LegacyFlags;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
@@ -79,6 +87,7 @@
 
 /** A class responsible for the life cycle and event handling of the pop-ups over TV view. */
 @UiThread
+@AutoFactory
 public class TvOverlayManager implements AccessibilityStateChangeListener {
     private static final String TAG = "TvOverlayManager";
     private static final boolean DEBUG = false;
@@ -216,6 +225,7 @@
 
     private final List<Runnable> mPendingActions = new ArrayList<>();
     private final Queue<PendingDialogAction> mPendingDialogActionQueue = new LinkedList<>();
+    private final LegacyFlags mLegacyFlags;
 
     private OnBackStackChangedListener mOnBackStackChangedListener;
 
@@ -229,12 +239,17 @@
             InputBannerView inputBannerView,
             SelectInputView selectInputView,
             ViewGroup sceneContainer,
-            ProgramGuideSearchFragment searchFragment) {
+            ProgramGuideSearchFragment searchFragment,
+            @Provided LegacyFlags legacyFlags,
+            @Provided ChannelDataManager channelDataManager,
+            @Provided TvInputManagerHelper tvInputManager,
+            @Provided ProgramDataManager programDataManager) {
         mMainActivity = mainActivity;
         mChannelTuner = channelTuner;
         TvSingletons singletons = TvSingletons.getSingletons(mainActivity);
-        mChannelDataManager = singletons.getChannelDataManager();
-        mInputManager = singletons.getTvInputManagerHelper();
+        mLegacyFlags = legacyFlags;
+        mChannelDataManager = channelDataManager;
+        mInputManager = tvInputManager;
         mTvView = tvView;
         mChannelBannerView = channelBannerView;
         mKeypadChannelSwitchView = keypadChannelSwitchView;
@@ -271,7 +286,7 @@
                         tvView,
                         optionsManager,
                         menuView,
-                        new MenuRowFactory(mainActivity, tvView),
+                        new MenuRowFactory(mainActivity, tvView, this.mLegacyFlags),
                         new Menu.OnMenuVisibilityChangeListener() {
                             @Override
                             public void onMenuVisibilityChange(boolean visible) {
@@ -304,9 +319,9 @@
                 new ProgramGuide(
                         mainActivity,
                         channelTuner,
-                        singletons.getTvInputManagerHelper(),
+                        mInputManager,
                         mChannelDataManager,
-                        singletons.getProgramDataManager(),
+                        programDataManager,
                         dvrDataManager,
                         singletons.getDvrScheduleManager(),
                         singletons.getTracker(),
diff --git a/src/com/android/tv/ui/ViewUtils.java b/src/com/android/tv/ui/ViewUtils.java
index f64a70b..64db7ff 100644
--- a/src/com/android/tv/ui/ViewUtils.java
+++ b/src/com/android/tv/ui/ViewUtils.java
@@ -18,9 +18,11 @@
 
 import android.animation.Animator;
 import android.animation.ValueAnimator;
+import android.os.Build;
 import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup.LayoutParams;
+
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 
@@ -33,6 +35,11 @@
     }
 
     public static void setTransitionAlpha(View v, float alpha) {
+        /* Begin_AOSP_Before_Q_Comment_Out */
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            v.setTransitionAlpha(alpha);
+        }
+        /* End_AOSP_Before_Q_Comment_Out */
         Method method;
         try {
             method = View.class.getDeclaredMethod("setTransitionAlpha", Float.TYPE);
diff --git a/src/com/android/tv/ui/sidepanel/ChannelCheckItem.java b/src/com/android/tv/ui/sidepanel/ChannelCheckItem.java
index 2726839..de3ae75 100644
--- a/src/com/android/tv/ui/sidepanel/ChannelCheckItem.java
+++ b/src/com/android/tv/ui/sidepanel/ChannelCheckItem.java
@@ -19,13 +19,14 @@
 import android.text.TextUtils;
 import android.view.View;
 import android.widget.TextView;
+
 import com.android.tv.R;
 import com.android.tv.data.ChannelDataManager;
 import com.android.tv.data.ChannelDataManager.ChannelListener;
 import com.android.tv.data.OnCurrentProgramUpdatedListener;
-import com.android.tv.data.Program;
 import com.android.tv.data.ProgramDataManager;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 
 public abstract class ChannelCheckItem extends CompoundButtonItem {
     private final ChannelDataManager mChannelDataManager;
diff --git a/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java b/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java
index 62130b6..b62a57e 100644
--- a/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java
+++ b/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java
@@ -20,7 +20,7 @@
 import android.content.SharedPreferences;
 import android.media.tv.TvContract.Channels;
 import android.os.Bundle;
-import android.support.v17.leanback.widget.VerticalGridView;
+import androidx.leanback.widget.VerticalGridView;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
diff --git a/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java b/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java
index 36ee5a2..e43568c 100644
--- a/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java
+++ b/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java
@@ -16,29 +16,38 @@
 
 package com.android.tv.ui.sidepanel;
 
-import android.accounts.Account;
 import android.app.Activity;
-import android.support.annotation.NonNull;
-import android.util.Log;
-import android.widget.Toast;
+
+import com.android.tv.MainActivity;
 import com.android.tv.R;
-import com.android.tv.TvSingletons;
+import com.android.tv.common.BuildConfig;
 import com.android.tv.common.CommonPreferences;
 import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.common.util.CommonUtils;
+import com.android.tv.perf.PerformanceMonitor;
 
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
 
+import dagger.android.AndroidInjection;
 
+import com.android.tv.common.flags.LegacyFlags;
 
-
-import java.util.ArrayList;
-import java.util.List;
+import javax.inject.Inject;
 
 /** Options for developers only */
 public class DeveloperOptionFragment extends SideFragment {
-    private static final String TAG = "DeveloperOptionFragment";
     private static final String TRACKER_LABEL = "debug options";
 
+    @Inject Optional<AdditionalDeveloperItemsFactory> mAdditionalDeveloperItemsFactory;
+    @Inject PerformanceMonitor mPerformanceMonitor;
+    @Inject LegacyFlags mLegacyFlags;
+
+    @Override
+    public void onAttach(Activity activity) {
+        AndroidInjection.inject(this);
+        super.onAttach(activity);
+    }
+
     @Override
     protected String getTitle() {
         return getString(R.string.menu_developer_options);
@@ -50,8 +59,15 @@
     }
 
     @Override
-    protected List<Item> getItemList() {
-        List<Item> items = new ArrayList<>();
+    protected ImmutableList<Item> getItemList() {
+        ImmutableList.Builder<Item> items = ImmutableList.builder();
+        if (mAdditionalDeveloperItemsFactory.isPresent()) {
+            items.addAll(
+                    mAdditionalDeveloperItemsFactory
+                            .get()
+                            .getAdditionalDevItems(getMainActivity()));
+            items.add(new DividerItem());
+        }
         if (CommonFeatures.DVR.isEnabled(getContext())) {
             items.add(
                     new ActionItem(getString(R.string.dev_item_dvr_history)) {
@@ -61,7 +77,7 @@
                         }
                     });
         }
-        if (CommonUtils.isDeveloper()) {
+        if (BuildConfig.ENG || mLegacyFlags.enableDeveloperFeatures()) {
             items.add(
                     new ActionItem(getString(R.string.dev_item_watch_history)) {
                         @Override
@@ -87,17 +103,21 @@
                         CommonPreferences.setStoreTsStream(getContext(), isChecked());
                     }
                 });
-        if (CommonUtils.isDeveloper()) {
+        if (BuildConfig.ENG || mLegacyFlags.enableDeveloperFeatures()) {
             items.add(
                     new ActionItem(getString(R.string.dev_item_show_performance_monitor_log)) {
                         @Override
                         protected void onSelected() {
-                            TvSingletons.getSingletons(getContext())
-                                    .getPerformanceMonitor()
-                                    .startPerformanceMonitorEventDebugActivity(getContext());
+                            mPerformanceMonitor.startPerformanceMonitorEventDebugActivity(
+                                    getContext());
                         }
                     });
         }
-        return items;
+        return items.build();
+    }
+
+    /** Factory to create additional items. */
+    public interface AdditionalDeveloperItemsFactory {
+        ImmutableList<Item> getAdditionalDevItems(MainActivity mainActivity);
     }
 }
diff --git a/src/com/android/tv/ui/sidepanel/SettingsFragment.java b/src/com/android/tv/ui/sidepanel/SettingsFragment.java
index aa71fb7..1c03b6a 100644
--- a/src/com/android/tv/ui/sidepanel/SettingsFragment.java
+++ b/src/com/android/tv/ui/sidepanel/SettingsFragment.java
@@ -35,7 +35,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
-/** Shows Live TV settings. */
+/** Shows TV app settings. */
 public class SettingsFragment extends SideFragment {
     private static final String TRACKER_LABEL = "settings";
 
diff --git a/src/com/android/tv/ui/sidepanel/SideFragment.java b/src/com/android/tv/ui/sidepanel/SideFragment.java
index 590f130..703b1e4 100644
--- a/src/com/android/tv/ui/sidepanel/SideFragment.java
+++ b/src/com/android/tv/ui/sidepanel/SideFragment.java
@@ -20,24 +20,27 @@
 import android.content.Context;
 import android.graphics.drawable.RippleDrawable;
 import android.os.Bundle;
-import android.support.v17.leanback.widget.VerticalGridView;
-import android.support.v7.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
 import android.widget.TextView;
+
+import androidx.leanback.widget.VerticalGridView;
+
 import com.android.tv.MainActivity;
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
 import com.android.tv.analytics.HasTrackerLabel;
 import com.android.tv.analytics.Tracker;
+import com.android.tv.common.dev.DeveloperPreferences;
 import com.android.tv.common.util.DurationTimer;
-import com.android.tv.common.util.SystemProperties;
 import com.android.tv.data.ChannelDataManager;
 import com.android.tv.data.ProgramDataManager;
 import com.android.tv.util.ViewCache;
+
 import java.util.List;
 
 public abstract class SideFragment<T extends Item> extends Fragment implements HasTrackerLabel {
@@ -56,7 +59,7 @@
             new RecyclerView.RecycledViewPool();
 
     private VerticalGridView mListView;
-    private ItemAdapter mAdapter;
+    private ItemAdapter<T> mAdapter;
     private SideFragmentListener mListener;
     private ChannelDataManager mChannelDataManager;
     private ProgramDataManager mProgramDataManager;
@@ -65,6 +68,7 @@
 
     private final int mHideKey;
     private final int mDebugHideKey;
+    private Context mContext;
 
     public SideFragment() {
         this(KeyEvent.KEYCODE_UNKNOWN, KeyEvent.KEYCODE_UNKNOWN);
@@ -73,7 +77,7 @@
     /**
      * @param hideKey the KeyCode used to hide the fragment
      * @param debugHideKey the KeyCode used to hide the fragment if {@link
-     *     SystemProperties#USE_DEBUG_KEYS}.
+     *     DeveloperPreferences#USE_DEBUG_KEYS}.
      */
     public SideFragment(int hideKey, int debugHideKey) {
         mHideKey = hideKey;
@@ -83,6 +87,7 @@
     @Override
     public void onAttach(Context context) {
         super.onAttach(context);
+        mContext = context;
         mChannelDataManager = getMainActivity().getChannelDataManager();
         mProgramDataManager = getMainActivity().getProgramDataManager();
         mTracker = TvSingletons.getSingletons(context).getTracker();
@@ -129,7 +134,8 @@
     }
 
     public final boolean isHideKeyForThisPanel(int keyCode) {
-        boolean debugKeysEnabled = SystemProperties.USE_DEBUG_KEYS.getValue();
+        boolean debugKeysEnabled =
+                DeveloperPreferences.USE_DEBUG_KEYS.getDefaultIfContextNull(mContext);
         return mHideKey != KeyEvent.KEYCODE_UNKNOWN
                 && (mHideKey == keyCode || (debugKeysEnabled && mDebugHideKey == keyCode));
     }
diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java
index b14bf78..620b701 100644
--- a/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java
+++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/ChannelsBlockedFragment.java
@@ -23,7 +23,7 @@
 import android.os.Build.VERSION_CODES;
 import android.os.Bundle;
 import android.os.Handler;
-import android.support.v17.leanback.widget.VerticalGridView;
+import androidx.leanback.widget.VerticalGridView;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java
index d1ae442..60f8425 100644
--- a/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java
+++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java
@@ -16,6 +16,7 @@
 
 package com.android.tv.ui.sidepanel.parentalcontrols;
 
+import android.app.Activity;
 import android.graphics.drawable.Drawable;
 import android.media.tv.TvContentRating;
 import android.os.Bundle;
@@ -24,9 +25,9 @@
 import android.view.View;
 import android.widget.CompoundButton;
 import android.widget.ImageView;
+
 import com.android.tv.MainActivity;
 import com.android.tv.R;
-import com.android.tv.common.experiments.Experiments;
 import com.android.tv.dialog.WebDialogFragment;
 import com.android.tv.license.LicenseUtils;
 import com.android.tv.parental.ContentRatingSystem;
@@ -39,18 +40,28 @@
 import com.android.tv.ui.sidepanel.SideFragment;
 import com.android.tv.util.TvSettings;
 import com.android.tv.util.TvSettings.ContentRatingLevel;
+
 import com.google.common.collect.ImmutableList;
+
+import dagger.android.AndroidInjection;
+
+import com.android.tv.common.flags.LegacyFlags;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
+import javax.inject.Inject;
+
 public class RatingsFragment extends SideFragment {
     private static final SparseIntArray sLevelResourceIdMap;
     private static final SparseIntArray sDescriptionResourceIdMap;
     private static final String TRACKER_LABEL = "Ratings";
     private int mItemsSize;
 
+    @Inject LegacyFlags mLegacyFlags;
+
     static {
         sLevelResourceIdMap = new SparseIntArray(5);
         sLevelResourceIdMap.put(TvSettings.CONTENT_RATING_LEVEL_NONE, R.string.option_rating_none);
@@ -101,8 +112,7 @@
     protected List<Item> getItemList() {
         List<Item> items = new ArrayList<>();
 
-        if (mBlockUnratedItem != null
-                && Boolean.TRUE.equals(Experiments.ENABLE_UNRATED_CONTENT_SETTINGS.get())) {
+        if (mBlockUnratedItem != null && mLegacyFlags.enableUnratedContentSettings()) {
             items.add(mBlockUnratedItem);
             items.add(new DividerItem());
         }
@@ -158,7 +168,13 @@
         super.onCreate(savedInstanceState);
         mParentalControlSettings = getMainActivity().getParentalControlSettings();
         mParentalControlSettings.loadRatings();
-        if (Boolean.TRUE.equals(Experiments.ENABLE_UNRATED_CONTENT_SETTINGS.get())) {
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        AndroidInjection.inject(this);
+        super.onAttach(activity);
+        if (mLegacyFlags.enableUnratedContentSettings()) {
             mBlockUnratedItem =
                     new CheckBoxItem(
                             getResources().getString(R.string.option_block_unrated_programs)) {
@@ -179,6 +195,8 @@
                             }
                         }
                     };
+        } else {
+            mBlockUnratedItem = null;
         }
     }
 
@@ -235,8 +253,7 @@
             super.onSelected();
             mParentalControlSettings.setContentRatingLevel(
                     getMainActivity().getContentRatingsManager(), mRatingLevel);
-            if (mBlockUnratedItem != null
-                    && Boolean.TRUE.equals(Experiments.ENABLE_UNRATED_CONTENT_SETTINGS.get())) {
+            if (mBlockUnratedItem != null && mLegacyFlags.enableUnratedContentSettings()) {
                 // set checked if UNRATED is blocked, and set unchecked otherwise.
                 mBlockUnratedItem.setChecked(
                         mParentalControlSettings.isRatingBlocked(
diff --git a/src/com/android/tv/util/AsyncDbTask.java b/src/com/android/tv/util/AsyncDbTask.java
index b352395..2e9a1ea 100644
--- a/src/com/android/tv/util/AsyncDbTask.java
+++ b/src/com/android/tv/util/AsyncDbTask.java
@@ -28,18 +28,23 @@
 import android.support.annotation.WorkerThread;
 import android.util.Log;
 import android.util.Range;
+
 import com.android.tv.TvSingletons;
 import com.android.tv.common.BuildConfig;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.data.ChannelImpl;
-import com.android.tv.data.Program;
+import com.android.tv.data.ProgramImpl;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 import com.android.tv.dvr.data.RecordedProgram;
+
 import com.google.common.base.Predicate;
+
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Executor;
+
 import javax.inject.Qualifier;
 
 /**
@@ -123,7 +128,7 @@
                 return null;
             }
             if (Utils.isProgramsUri(mUri)
-                            && TvProviderUtils.checkSeriesIdColumn(context, Programs.CONTENT_URI)) {
+                    && TvProviderUtils.checkSeriesIdColumn(context, Programs.CONTENT_URI)) {
                 mProjection =
                         TvProviderUtils.addExtraColumnsToProjection(
                                 mProjection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID);
@@ -323,10 +328,19 @@
         }
     }
 
-    /** Gets an {@link List} of {@link Program}s from {@link TvContract.Programs#CONTENT_URI}. */
+    /**
+     * Gets an {@link List} of {@link ProgramImpl}s from {@link TvContract.Programs#CONTENT_URI}.
+     */
     public abstract static class AsyncProgramQueryTask extends AsyncQueryListTask<Program> {
         public AsyncProgramQueryTask(Executor executor, Context context) {
-            super(executor, context, Programs.CONTENT_URI, Program.PROJECTION, null, null, null);
+            super(
+                    executor,
+                    context,
+                    Programs.CONTENT_URI,
+                    ProgramImpl.PROJECTION,
+                    null,
+                    null,
+                    null);
         }
 
         public AsyncProgramQueryTask(
@@ -341,7 +355,7 @@
                     executor,
                     context,
                     uri,
-                    Program.PROJECTION,
+                    ProgramImpl.PROJECTION,
                     selection,
                     selectionArgs,
                     sortOrder,
@@ -350,7 +364,7 @@
 
         @Override
         protected final Program fromCursor(Cursor c) {
-            return Program.fromCursor(c);
+            return ProgramImpl.fromCursor(c);
         }
     }
 
@@ -376,7 +390,7 @@
     }
 
     /**
-     * Gets an {@link List} of {@link Program}s for a given channel and period {@link
+     * Gets an {@link List} of {@link ProgramImpl}s for a given channel and period {@link
      * TvContract#buildProgramsUriForChannel(long, long, long)}. If the {@code period} is {@code
      * null}, then all the programs is queried.
      */
@@ -410,7 +424,7 @@
         }
     }
 
-    /** Gets a single {@link Program} from {@link TvContract.Programs#CONTENT_URI}. */
+    /** Gets a single {@link ProgramImpl} from {@link TvContract.Programs#CONTENT_URI}. */
     public static class AsyncQueryProgramTask extends AsyncQueryItemTask<Program> {
 
         public AsyncQueryProgramTask(Executor executor, Context context, long programId) {
@@ -418,7 +432,7 @@
                     executor,
                     context,
                     TvContract.buildProgramUri(programId),
-                    Program.PROJECTION,
+                    ProgramImpl.PROJECTION,
                     null,
                     null,
                     null);
@@ -426,7 +440,7 @@
 
         @Override
         protected Program fromCursor(Cursor c) {
-            return Program.fromCursor(c);
+            return ProgramImpl.fromCursor(c);
         }
     }
 
diff --git a/src/com/android/tv/util/OnboardingUtils.java b/src/com/android/tv/util/OnboardingUtils.java
index 3b72e09..4fae2f0 100644
--- a/src/com/android/tv/util/OnboardingUtils.java
+++ b/src/com/android/tv/util/OnboardingUtils.java
@@ -27,7 +27,9 @@
     private static final String PREF_KEY_ONBOARDING_VERSION_CODE = "pref_onbaording_versionCode";
     private static final int ONBOARDING_VERSION = 1;
 
-    private static final String MERCHANT_COLLECTION_URL_STRING = getMerchantCollectionUrl();
+    // Replace as needed
+    private static final String MERCHANT_COLLECTION_URL_STRING =
+            "https://play.google.com/store/apps/collection/promotion_3001bf9_ATV_livechannels";
 
     /** Intent to show merchant collection in online store. */
     public static final Intent ONLINE_STORE_INTENT =
@@ -69,10 +71,5 @@
                 .apply();
     }
 
-    /** Returns merchant collection URL. */
-    private static String getMerchantCollectionUrl() {
-        return "TODO: add a merchant collection url";
-    }
-
     private OnboardingUtils() {}
 }
diff --git a/src/com/android/tv/util/SetupUtils.java b/src/com/android/tv/util/SetupUtils.java
index a9b67fa..52b3e3e 100644
--- a/src/com/android/tv/util/SetupUtils.java
+++ b/src/com/android/tv/util/SetupUtils.java
@@ -307,8 +307,7 @@
     }
 
     /**
-     * Called when Live channels app is launched. Once it is called, {@link #isFirstTune} will
-     * return false.
+     * Called when TV app is launched. Once it is called, {@link #isFirstTune} will return false.
      */
     public void onTuned() {
         if (!mIsFirstTune) {
diff --git a/src/com/android/tv/util/TvInputManagerHelper.java b/src/com/android/tv/util/TvInputManagerHelper.java
index cb7d985..23c9b49 100644
--- a/src/com/android/tv/util/TvInputManagerHelper.java
+++ b/src/com/android/tv/util/TvInputManagerHelper.java
@@ -46,6 +46,8 @@
 import com.android.tv.parental.ParentalControlSettings;
 import com.android.tv.util.images.ImageCache;
 import com.android.tv.util.images.ImageLoader;
+import com.google.common.collect.Ordering;
+import com.android.tv.common.flags.LegacyFlags;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -127,6 +129,7 @@
     private static final String PERMISSION_ACCESS_ALL_EPG_DATA =
             "com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA";
     private static final String[] mPhysicalTunerBlackList = {
+        "com.google.android.videos", // Play Movies
     };
     private static final String META_LABEL_SORT_KEY = "input_sort_key";
 
@@ -158,6 +161,10 @@
     }
 
     private static final String[] PARTNER_TUNER_INPUT_PREFIX_BLACKLIST = {
+        /* Begin_AOSP_Comment_Out
+        // Disabled partner's tuner input prefix list.
+        "com.mediatek.tvinput/.dtv"
+        End_AOSP_Comment_Out */
     };
 
     private static final String[] TESTABLE_INPUTS = {
@@ -292,8 +299,8 @@
     private boolean mAllow3rdPartyInputs;
 
     @Inject
-    public TvInputManagerHelper(@ApplicationContext Context context) {
-        this(context, createTvInputManagerWrapper(context));
+    public TvInputManagerHelper(@ApplicationContext Context context, LegacyFlags legacyFlags) {
+        this(context, createTvInputManagerWrapper(context), legacyFlags);
     }
 
     @Nullable
@@ -305,12 +312,14 @@
 
     @VisibleForTesting
     protected TvInputManagerHelper(
-            Context context, @Nullable TvInputManagerInterface tvInputManager) {
+            Context context,
+            @Nullable TvInputManagerInterface tvInputManager,
+            LegacyFlags legacyFlags) {
         mContext = context.getApplicationContext();
         mPackageManager = context.getPackageManager();
         mTvInputManager = tvInputManager;
         mContentRatingsManager = new ContentRatingsManager(context, tvInputManager);
-        mParentalControlSettings = new ParentalControlSettings(context);
+        mParentalControlSettings = new ParentalControlSettings(context, legacyFlags);
         mTvInputInfoComparator = new InputComparatorInternal(this);
         mContentObserver =
                 new ContentObserver(mHandler) {
@@ -348,7 +357,6 @@
         updateAllow3rdPartyInputs();
         mTvInputManager.registerCallback(mInternalCallback, mHandler);
         initInputMaps();
-        mContentRatingsManager.update();
     }
 
     public void stop() {
@@ -446,10 +454,12 @@
     }
 
     /** Loads label of {@code info}. */
+    @Nullable
     public String loadLabel(TvInputInfo info) {
         String label = mTvInputLabels.get(info.getId());
         if (label == null) {
-            label = info.loadLabel(mContext).toString();
+            CharSequence labelSequence = info.loadLabel(mContext);
+            label = labelSequence == null ? null : labelSequence.toString();
             mTvInputLabels.put(info.getId(), label);
         }
         return label;
@@ -703,6 +713,8 @@
     @VisibleForTesting
     static class InputComparatorInternal implements Comparator<TvInputInfo> {
         private final TvInputManagerHelper mInputManager;
+        private static final Ordering<Comparable> NULL_FIRST_STRING_ORDERING =
+                Ordering.natural().nullsFirst();
 
         public InputComparatorInternal(TvInputManagerHelper inputManager) {
             mInputManager = inputManager;
@@ -713,7 +725,8 @@
             if (mInputManager.isPartnerInput(lhs) != mInputManager.isPartnerInput(rhs)) {
                 return mInputManager.isPartnerInput(lhs) ? -1 : 1;
             }
-            return mInputManager.loadLabel(lhs).compareTo(mInputManager.loadLabel(rhs));
+            return NULL_FIRST_STRING_ORDERING.compare(
+                    mInputManager.loadLabel(lhs), mInputManager.loadLabel(rhs));
         }
     }
 
@@ -795,7 +808,7 @@
             if (TextUtils.isEmpty(label)) {
                 label = mTvInputManagerHelper.loadLabel(input);
             }
-            return label;
+            return label == null ? "" : label;
         }
 
         private int getPriority(TvInputInfo info) {
diff --git a/src/com/android/tv/util/TvProviderUtils.java b/src/com/android/tv/util/TvProviderUtils.java
index 6b5aaec..d931dcd 100644
--- a/src/com/android/tv/util/TvProviderUtils.java
+++ b/src/com/android/tv/util/TvProviderUtils.java
@@ -27,7 +27,7 @@
 import android.support.annotation.VisibleForTesting;
 import android.support.annotation.WorkerThread;
 import android.util.Log;
-import com.android.tv.data.BaseProgram;
+import com.android.tv.data.api.BaseProgram;
 import com.android.tv.features.PartnerFeatures;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -67,6 +67,9 @@
     @WorkerThread
     public static synchronized boolean checkSeriesIdColumn(Context context, Uri uri) {
         boolean canCreateColumn = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O);
+        canCreateColumn =
+                (canCreateColumn
+                        || PartnerFeatures.TVPROVIDER_ALLOWS_COLUMN_CREATION.isEnabled(context));
         if (!canCreateColumn) {
             return false;
         }
@@ -112,6 +115,9 @@
     @WorkerThread
     public static synchronized boolean checkStateColumn(Context context, Uri uri) {
         boolean canCreateColumn = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O);
+        canCreateColumn =
+                (canCreateColumn
+                        || PartnerFeatures.TVPROVIDER_ALLOWS_COLUMN_CREATION.isEnabled(context));
         if (!canCreateColumn) {
             return false;
         }
@@ -144,8 +150,8 @@
         return TRUE.equals(sRecordedProgramHasStateColumn);
     }
 
-    public static String[] addExtraColumnsToProjection(String[] projection,
-            @TvProviderExtraColumn String column) {
+    public static String[] addExtraColumnsToProjection(
+            String[] projection, @TvProviderExtraColumn String column) {
         List<String> projectionList = new ArrayList<>(Arrays.asList(projection));
         if (!projectionList.contains(column)) {
             projectionList.add(column);
diff --git a/src/com/android/tv/util/Utils.java b/src/com/android/tv/util/Utils.java
index 5117373..c1f9e93 100644
--- a/src/com/android/tv/util/Utils.java
+++ b/src/com/android/tv/util/Utils.java
@@ -39,17 +39,19 @@
 import android.text.format.DateUtils;
 import android.util.Log;
 import android.view.View;
+
 import com.android.tv.R;
 import com.android.tv.TvSingletons;
 import com.android.tv.common.BaseSingletons;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.util.Clock;
 import com.android.tv.data.GenreItems;
-import com.android.tv.data.Program;
+import com.android.tv.data.ProgramImpl;
 import com.android.tv.data.StreamInfo;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
+
 import java.text.SimpleDateFormat;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Calendar;
 import java.util.Collection;
@@ -324,7 +326,7 @@
                         TvContract.buildChannelUri(channelId), timeMs, timeMs);
         ContentResolver resolver = context.getContentResolver();
 
-        String[] projection = Program.PROJECTION;
+        String[] projection = ProgramImpl.PROJECTION;
         if (TvProviderUtils.checkSeriesIdColumn(context, TvContract.Programs.CONTENT_URI)) {
             if (Utils.isProgramsUri(uri)) {
                 projection =
@@ -334,7 +336,7 @@
         }
         try (Cursor cursor = resolver.query(uri, projection, null, null, null)) {
             if (cursor != null && cursor.moveToNext()) {
-                return Program.fromCursor(cursor);
+                return ProgramImpl.fromCursor(cursor);
             }
         }
         return null;
@@ -603,6 +605,7 @@
     }
 
     /** Returns the label for a given input. Returns the custom label, if any. */
+    @Nullable
     public static String loadLabel(Context context, TvInputInfo input) {
         if (input == null) {
             return null;
@@ -612,7 +615,7 @@
         CharSequence customLabel = inputManager.loadCustomLabel(input);
         String label = (customLabel == null) ? null : customLabel.toString();
         if (TextUtils.isEmpty(label)) {
-            label = inputManager.loadLabel(input).toString();
+            label = inputManager.loadLabel(input);
         }
         return label;
     }
@@ -714,33 +717,6 @@
         return context.createConfigurationContext(config).getText(resourceId);
     }
 
-    /** Checks where there is any internal TV input. */
-    public static boolean hasInternalTvInputs(Context context, boolean tunerInputOnly) {
-        for (TvInputInfo input :
-                TvSingletons.getSingletons(context)
-                        .getTvInputManagerHelper()
-                        .getTvInputInfos(true, tunerInputOnly)) {
-            if (isInternalTvInput(context, input.getId())) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /** Returns the internal TV inputs. */
-    public static List<TvInputInfo> getInternalTvInputs(Context context, boolean tunerInputOnly) {
-        List<TvInputInfo> inputs = new ArrayList<>();
-        for (TvInputInfo input :
-                TvSingletons.getSingletons(context)
-                        .getTvInputManagerHelper()
-                        .getTvInputInfos(true, tunerInputOnly)) {
-            if (isInternalTvInput(context, input.getId())) {
-                inputs.add(input);
-            }
-        }
-        return inputs;
-    }
-
     /** Checks whether the input is internal or not. */
     public static boolean isInternalTvInput(Context context, String inputId) {
         ComponentName unflattenInputId = ComponentName.unflattenFromString(inputId);
diff --git a/src/com/android/tv/util/account/AccountHelper.java b/src/com/android/tv/util/account/AccountHelper.java
index e98b42e..d54c2f5 100644
--- a/src/com/android/tv/util/account/AccountHelper.java
+++ b/src/com/android/tv/util/account/AccountHelper.java
@@ -35,4 +35,11 @@
     /** Returns all eligible accounts . */
     @Nullable
     Account getFirstEligibleAccount();
+
+    /**
+     * Initialize the account helper.
+     *
+     * <p>This method is a no op if called more than once.
+     */
+    void init();
 }
diff --git a/src/com/android/tv/util/account/AccountHelperImpl.java b/src/com/android/tv/util/account/AccountHelperImpl.java
index 58fbd27..e97cc3f 100644
--- a/src/com/android/tv/util/account/AccountHelperImpl.java
+++ b/src/com/android/tv/util/account/AccountHelperImpl.java
@@ -21,8 +21,12 @@
 import android.content.SharedPreferences;
 import android.preference.PreferenceManager;
 import android.support.annotation.Nullable;
+import com.android.tv.common.dagger.annotations.ApplicationContext;
+import javax.inject.Inject;
+import javax.inject.Singleton;
 
 /** Helper methods for getting and selecting a user account. */
+@Singleton
 public class AccountHelperImpl implements com.android.tv.util.account.AccountHelper {
     private static final String SELECTED_ACCOUNT = "android.tv.livechannels.selected_account";
 
@@ -31,7 +35,8 @@
 
     @Nullable private Account mSelectedAccount;
 
-    public AccountHelperImpl(Context context) {
+    @Inject
+    public AccountHelperImpl(@ApplicationContext Context context) {
         mContext = context.getApplicationContext();
         mDefaultPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
     }
@@ -98,4 +103,9 @@
                 PreferenceManager.getDefaultSharedPreferences(mContext);
         defaultPreferences.edit().putString(SELECTED_ACCOUNT, account.name).commit();
     }
+
+    @Override
+    public void init() {
+        // do nothing.
+    }
 }
diff --git a/src/com/android/tv/util/images/ImageLoader.java b/src/com/android/tv/util/images/ImageLoader.java
index d2ad0eb..e026f26 100644
--- a/src/com/android/tv/util/images/ImageLoader.java
+++ b/src/com/android/tv/util/images/ImageLoader.java
@@ -29,9 +29,13 @@
 import android.support.annotation.WorkerThread;
 import android.util.ArraySet;
 import android.util.Log;
+
+import androidx.tvprovider.media.tv.TvContractCompat.PreviewPrograms;
+
 import com.android.tv.R;
 import com.android.tv.common.concurrent.NamedThreadFactory;
 import com.android.tv.util.images.BitmapUtils.ScaledBitmapInfo;
+
 import java.lang.ref.WeakReference;
 import java.util.HashMap;
 import java.util.Map;
@@ -164,8 +168,8 @@
      * @return {@code true} if the load is complete and the callback is executed.
      */
     @UiThread
-    public static boolean loadBitmap(
-            Context context, String uriString, ImageLoaderCallback callback) {
+    public static <T> boolean loadBitmap(
+            Context context, String uriString, ImageLoaderCallback<T> callback) {
         return loadBitmap(context, uriString, Integer.MAX_VALUE, Integer.MAX_VALUE, callback);
     }
 
@@ -178,12 +182,12 @@
      * @return {@code true} if the load is complete and the callback is executed.
      */
     @UiThread
-    public static boolean loadBitmap(
+    public static <T> boolean loadBitmap(
             Context context,
             String uriString,
             int maxWidth,
             int maxHeight,
-            ImageLoaderCallback callback) {
+            ImageLoaderCallback<T> callback) {
         if (DEBUG) {
             Log.d(TAG, "loadBitmap() " + uriString);
         }
@@ -191,12 +195,12 @@
                 context, uriString, maxWidth, maxHeight, callback, IMAGE_THREAD_POOL_EXECUTOR);
     }
 
-    private static boolean doLoadBitmap(
+    private static <T> boolean doLoadBitmap(
             Context context,
             String uriString,
             int maxWidth,
             int maxHeight,
-            ImageLoaderCallback callback,
+            ImageLoaderCallback<T> callback,
             Executor executor) {
         // Check the cache before creating a Task.  The cache will be checked again in doLoadBitmap
         // but checking a cache is much cheaper than creating an new task.
@@ -222,7 +226,8 @@
      * @return {@code true} if the load is complete and the callback is executed.
      */
     @UiThread
-    public static boolean loadBitmap(ImageLoaderCallback callback, LoadBitmapTask loadBitmapTask) {
+    public static <T> boolean loadBitmap(
+            ImageLoaderCallback<T> callback, LoadBitmapTask<T> loadBitmapTask) {
         if (DEBUG) {
             Log.d(TAG, "loadBitmap() " + loadBitmapTask);
         }
@@ -231,8 +236,8 @@
 
     /** @return {@code true} if the load is complete and the callback is executed. */
     @UiThread
-    private static boolean doLoadBitmap(
-            ImageLoaderCallback callback, Executor executor, LoadBitmapTask loadBitmapTask) {
+    private static <T> boolean doLoadBitmap(
+            ImageLoaderCallback<T> callback, Executor executor, LoadBitmapTask<T> loadBitmapTask) {
         ScaledBitmapInfo bitmapInfo = loadBitmapTask.getFromCache();
         boolean needToReload = loadBitmapTask.isReloadNeeded();
         if (bitmapInfo != null && !needToReload) {
@@ -267,11 +272,11 @@
      *
      * <p>Implement {@link #doGetBitmapInBackground} to do the actual loading.
      */
-    public abstract static class LoadBitmapTask extends AsyncTask<Void, Void, ScaledBitmapInfo> {
+    public abstract static class LoadBitmapTask<T> extends AsyncTask<Void, Void, ScaledBitmapInfo> {
         protected final Context mAppContext;
         protected final int mMaxWidth;
         protected final int mMaxHeight;
-        private final Set<ImageLoaderCallback> mCallbacks = new ArraySet<>();
+        private final Set<ImageLoaderCallback<T>> mCallbacks = new ArraySet<>();
         private final ImageCache mImageCache;
         private final String mKey;
 
@@ -353,7 +358,7 @@
         public final void onPostExecute(ScaledBitmapInfo scaledBitmapInfo) {
             if (DEBUG) Log.d(ImageLoader.TAG, "Bitmap is loaded " + mKey);
 
-            for (ImageLoader.ImageLoaderCallback callback : mCallbacks) {
+            for (ImageLoader.ImageLoaderCallback<T> callback : mCallbacks) {
                 callback.onBitmapLoaded(scaledBitmapInfo == null ? null : scaledBitmapInfo.bitmap);
             }
             ImageLoader.sPendingListMap.remove(mKey);
@@ -376,7 +381,7 @@
         }
     }
 
-    private static final class LoadBitmapFromUriTask extends LoadBitmapTask {
+    private static final class LoadBitmapFromUriTask<T> extends LoadBitmapTask<T> {
         private LoadBitmapFromUriTask(
                 Context context,
                 ImageCache imageCache,
@@ -395,7 +400,7 @@
     }
 
     /** Loads and caches the logo for a given {@link TvInputInfo} */
-    public static final class LoadTvInputLogoTask extends LoadBitmapTask {
+    public static final class LoadTvInputLogoTask<T> extends LoadBitmapTask<T> {
         private final TvInputInfo mInfo;
 
         public LoadTvInputLogoTask(Context context, ImageCache cache, TvInputInfo info) {
@@ -414,9 +419,10 @@
         @Override
         public ScaledBitmapInfo doGetBitmapInBackground() {
             Drawable drawable = mInfo.loadIcon(mAppContext);
-            Bitmap bm = drawable instanceof BitmapDrawable
-                    ? ((BitmapDrawable) drawable).getBitmap()
-                    : BitmapUtils.drawableToBitmap(drawable);
+            Bitmap bm =
+                    drawable instanceof BitmapDrawable
+                            ? ((BitmapDrawable) drawable).getBitmap()
+                            : BitmapUtils.drawableToBitmap(drawable);
             return bm == null
                     ? null
                     : BitmapUtils.createScaledBitmapInfo(getKey(), bm, mMaxWidth, mMaxHeight);
@@ -428,6 +434,46 @@
         }
     }
 
+    /**
+     * Calculates Aspect Ratio of Poster Art from Uri.
+     *
+     * <p><b>Note</b> the function will check the cache before loading the bitmap
+     *
+     * @return the Aspect Ratio of the Poster Art.
+     */
+    public static int getAspectRatioFromPosterArtUri(Context context, String uriString) {
+        // Check the cache before loading the bitmap.
+        ImageCache imageCache = ImageCache.getInstance();
+        ScaledBitmapInfo bitmapInfo = imageCache.get(uriString);
+        int bitmapWidth;
+        int bitmapHeight;
+        float bitmapAspectRatio;
+        int aspectRatio;
+        if (bitmapInfo == null) {
+            bitmapInfo =
+                    BitmapUtils.decodeSampledBitmapFromUriString(
+                            context, uriString, Integer.MAX_VALUE, Integer.MAX_VALUE);
+        }
+        bitmapWidth = bitmapInfo.bitmap.getWidth();
+        bitmapHeight = bitmapInfo.bitmap.getHeight();
+        bitmapAspectRatio = (float) bitmapWidth / bitmapHeight;
+        /* Assign nearest aspect ratio from the defined values in Preview Programs */
+        if (bitmapAspectRatio > 0 && bitmapAspectRatio <= 0.6803) {
+            aspectRatio = PreviewPrograms.ASPECT_RATIO_2_3;
+        } else if (bitmapAspectRatio > 0.6803 && bitmapAspectRatio <= 0.8469) {
+            aspectRatio = PreviewPrograms.ASPECT_RATIO_MOVIE_POSTER;
+        } else if (bitmapAspectRatio > 0.8469 && bitmapAspectRatio <= 1.1667) {
+            aspectRatio = PreviewPrograms.ASPECT_RATIO_1_1;
+        } else if (bitmapAspectRatio > 1.1667 && bitmapAspectRatio <= 1.4167) {
+            aspectRatio = PreviewPrograms.ASPECT_RATIO_4_3;
+        } else if (bitmapAspectRatio > 1.4167 && bitmapAspectRatio <= 1.6389) {
+            aspectRatio = PreviewPrograms.ASPECT_RATIO_3_2;
+        } else {
+            aspectRatio = PreviewPrograms.ASPECT_RATIO_16_9;
+        }
+        return aspectRatio;
+    }
+
     private static synchronized Handler getMainHandler() {
         if (sMainHandler == null) {
             sMainHandler = new Handler(Looper.getMainLooper());
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
new file mode 100644
index 0000000..a3b6bb2
--- /dev/null
+++ b/tests/common/Android.bp
@@ -0,0 +1,50 @@
+//
+// 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-test-common",
+
+    // Include all test java files.
+    srcs: [
+        "src/**/*.java",
+        "src/**/I*.aidl",
+    ],
+
+    static_libs: [
+        "android-support-annotations",
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "tv-guava-android-jar",
+        "mockito-target",
+        "tv-lib-truth",
+        "ub-uiautomator",
+        "Robolectric_all-target",
+    ],
+
+    // Link tv-common as shared library to avoid the problem of initialization of the constants
+    libs: [
+        "tv-common",
+        "LiveTv",
+    ],
+
+    sdk_version: "system_current",
+
+    resource_dirs: ["res"],
+    aidl: {
+        local_include_dirs: ["src"],
+    },
+
+}
diff --git a/tests/common/Android.mk b/tests/common/Android.mk
deleted file mode 100644
index 7a111d0..0000000
--- a/tests/common/Android.mk
+++ /dev/null
@@ -1,29 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-# Include all test java files.
-LOCAL_SRC_FILES := \
-    $(call all-java-files-under, src) \
-    $(call all-Iaidl-files-under, src)
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
-    android-support-annotations \
-    androidx.test.runner \
-    androidx.test.rules \
-    tv-guava-android-jar \
-    mockito-target \
-    tv-lib-truth \
-    ub-uiautomator \
-
-# Link tv-common as shared library to avoid the problem of initialization of the constants
-LOCAL_JAVA_LIBRARIES := tv-common
-
-LOCAL_INSTRUMENTATION_FOR := LiveTv
-LOCAL_MODULE := tv-test-common
-LOCAL_MODULE_TAGS := optional
-LOCAL_SDK_VERSION := system_current
-
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-LOCAL_AIDL_INCLUDES += $(LOCAL_PATH)/src
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tests/common/AndroidManifest.xml b/tests/common/AndroidManifest.xml
index 3a769a8..ec9614d 100644
--- a/tests/common/AndroidManifest.xml
+++ b/tests/common/AndroidManifest.xml
@@ -18,6 +18,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.tv.testing"
           android:versionCode="1">
-  <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
+  <uses-sdk android:targetSdkVersion="28" android:minSdkVersion="23"/>
     <application />
 </manifest>
diff --git a/tests/common/src/com/android/tv/testing/ChannelNumberSubject.java b/tests/common/src/com/android/tv/testing/ChannelNumberSubject.java
index ba4662e..1592897 100644
--- a/tests/common/src/com/android/tv/testing/ChannelNumberSubject.java
+++ b/tests/common/src/com/android/tv/testing/ChannelNumberSubject.java
@@ -16,6 +16,8 @@
 
 package com.android.tv.testing;
 
+import static com.google.common.truth.Fact.simpleFact;
+
 import android.support.annotation.Nullable;
 import com.android.tv.data.ChannelNumber;
 import com.google.common.truth.ComparableSubject;
@@ -24,8 +26,7 @@
 import com.google.common.truth.Truth;
 
 /** Propositions for {@link ChannelNumber} subjects. */
-public final class ChannelNumberSubject
-        extends ComparableSubject<ChannelNumberSubject, ChannelNumber> {
+public final class ChannelNumberSubject extends ComparableSubject {
     private static final Subject.Factory<ChannelNumberSubject, ChannelNumber> FACTORY =
             ChannelNumberSubject::new;
 
@@ -37,30 +38,30 @@
         return Truth.assertAbout(channelNumbers()).that(actual);
     }
 
-    public ChannelNumberSubject(FailureMetadata failureMetadata, @Nullable ChannelNumber subject) {
-        super(failureMetadata, subject);
+  private final ChannelNumber actual;
+
+  public ChannelNumberSubject(FailureMetadata failureMetadata, @Nullable ChannelNumber subject) {
+    super(failureMetadata, subject);
+    this.actual = subject;
     }
 
     public void displaysAs(int major) {
-        if (!getSubject().majorNumber.equals(Integer.toString(major))
-                || getSubject().hasDelimiter) {
-            fail("displaysAs", major);
+    if (!actual.majorNumber.equals(Integer.toString(major)) || actual.hasDelimiter) {
+            failWithActual("expected to display as", major);
         }
     }
 
     public void displaysAs(int major, int minor) {
-        if (!getSubject().majorNumber.equals(Integer.toString(major))
-                || !getSubject().minorNumber.equals(Integer.toString(minor))
-                || !getSubject().hasDelimiter) {
-            fail("displaysAs", major + "-" + minor);
+    if (!actual.majorNumber.equals(Integer.toString(major))
+        || !actual.minorNumber.equals(Integer.toString(minor))
+        || !actual.hasDelimiter) {
+            failWithActual("expected to display as", major + "-" + minor);
         }
     }
 
     public void isEmpty() {
-        if (!getSubject().majorNumber.isEmpty()
-                || !getSubject().minorNumber.isEmpty()
-                || getSubject().hasDelimiter) {
-            fail("isEmpty");
+    if (!actual.majorNumber.isEmpty() || !actual.minorNumber.isEmpty() || actual.hasDelimiter) {
+            failWithActual(simpleFact("expected to be empty"));
         }
     }
 }
diff --git a/tests/common/src/com/android/tv/testing/EpgTestData.java b/tests/common/src/com/android/tv/testing/EpgTestData.java
index 362f336..d22bd28 100644
--- a/tests/common/src/com/android/tv/testing/EpgTestData.java
+++ b/tests/common/src/com/android/tv/testing/EpgTestData.java
@@ -18,13 +18,17 @@
 
 import com.android.tv.data.ChannelImpl;
 import com.android.tv.data.Lineup;
-import com.android.tv.data.Program;
+import com.android.tv.data.ProgramImpl;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
+import com.android.tv.testing.fakes.FakeClock;
+
 import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
+
 import java.util.concurrent.TimeUnit;
 
 /** EPG data for use in tests. */
@@ -75,14 +79,14 @@
     // Start and end time may be negative meaning they happen before "now".
 
     public static final Program PROGRAM_1 =
-            new Program.Builder()
+            new ProgramImpl.Builder()
                     .setTitle("Program 1")
                     .setStartTimeUtcMillis(0)
                     .setEndTimeUtcMillis(TimeUnit.MINUTES.toMillis(30))
                     .build();
 
     public static final Program PROGRAM_2 =
-            new Program.Builder()
+            new ProgramImpl.Builder()
                     .setTitle("Program 2")
                     .setStartTimeUtcMillis(TimeUnit.MINUTES.toMillis(30))
                     .setEndTimeUtcMillis(TimeUnit.MINUTES.toMillis(60))
@@ -191,7 +195,7 @@
                 new Function<Program, Program>() {
                     @Override
                     public Program apply(Program p) {
-                        return new Program.Builder(p)
+                        return new ProgramImpl.Builder(p)
                                 .setStartTimeUtcMillis(p.getStartTimeUtcMillis() + time)
                                 .setEndTimeUtcMillis(p.getEndTimeUtcMillis() + time)
                                 .build();
diff --git a/tests/common/src/com/android/tv/testing/FakeEpgReader.java b/tests/common/src/com/android/tv/testing/FakeEpgReader.java
index fb35c65..24afe8e 100644
--- a/tests/common/src/com/android/tv/testing/FakeEpgReader.java
+++ b/tests/common/src/com/android/tv/testing/FakeEpgReader.java
@@ -19,13 +19,17 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.util.Range;
+
 import com.android.tv.data.ChannelImpl;
 import com.android.tv.data.ChannelNumber;
 import com.android.tv.data.Lineup;
-import com.android.tv.data.Program;
+import com.android.tv.data.ProgramImpl;
 import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
 import com.android.tv.data.epg.EpgReader;
 import com.android.tv.dvr.data.SeriesInfo;
+import com.android.tv.testing.fakes.FakeClock;
+
 import com.google.common.base.Function;
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
@@ -33,6 +37,7 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
+
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
@@ -144,7 +149,7 @@
             @Nullable
             @Override
             public Program apply(@Nullable Program program) {
-                return new Program.Builder(program)
+                return new ProgramImpl.Builder(program)
                         .setChannelId(channel.getChannel().getId())
                         .setPackageName(channel.getChannel().getPackageName())
                         .build();
diff --git a/tests/common/src/com/android/tv/testing/FakeTvInputManagerHelper.java b/tests/common/src/com/android/tv/testing/FakeTvInputManagerHelper.java
index 85bdcf0..be08385 100644
--- a/tests/common/src/com/android/tv/testing/FakeTvInputManagerHelper.java
+++ b/tests/common/src/com/android/tv/testing/FakeTvInputManagerHelper.java
@@ -17,13 +17,14 @@
 package com.android.tv.testing;
 
 import android.content.Context;
+import com.android.tv.common.flags.impl.DefaultLegacyFlags;
 import com.android.tv.util.TvInputManagerHelper;
 
 /** Fake TvInputManagerHelper. */
 public class FakeTvInputManagerHelper extends TvInputManagerHelper {
 
     public FakeTvInputManagerHelper(Context context) {
-        super(context, new FakeTvInputManager());
+        super(context, new FakeTvInputManager(), DefaultLegacyFlags.DEFAULT);
     }
 
     public FakeTvInputManager getFakeTvInputManager() {
diff --git a/tests/common/src/com/android/tv/testing/TestSingletonApp.java b/tests/common/src/com/android/tv/testing/TestSingletonApp.java
index f1a98ff..e233d95 100644
--- a/tests/common/src/com/android/tv/testing/TestSingletonApp.java
+++ b/tests/common/src/com/android/tv/testing/TestSingletonApp.java
@@ -19,25 +19,23 @@
 import android.app.Application;
 import android.media.tv.TvInputManager;
 import android.os.AsyncTask;
+
 import com.android.tv.InputSessionManager;
 import com.android.tv.MainActivityWrapper;
 import com.android.tv.TvSingletons;
 import com.android.tv.analytics.Analytics;
 import com.android.tv.analytics.Tracker;
 import com.android.tv.common.BaseApplication;
-import com.android.tv.common.experiments.ExperimentLoader;
 import com.android.tv.common.flags.impl.DefaultBackendKnobsFlags;
 import com.android.tv.common.flags.impl.DefaultCloudEpgFlags;
-import com.android.tv.common.flags.impl.DefaultConcurrentDvrPlaybackFlags;
-import com.android.tv.common.flags.impl.DefaultTunerFlags;
 import com.android.tv.common.flags.impl.DefaultUiFlags;
+import com.android.tv.common.flags.impl.SettableFlagsModule;
 import com.android.tv.common.recording.RecordingStorageStatusManager;
 import com.android.tv.common.singletons.HasSingletons;
 import com.android.tv.common.util.Clock;
 import com.android.tv.data.ChannelDataManager;
 import com.android.tv.data.PreviewDataManager;
 import com.android.tv.data.ProgramDataManager;
-import com.android.tv.data.epg.EpgFetcher;
 import com.android.tv.data.epg.EpgReader;
 import com.android.tv.dvr.DvrDataManager;
 import com.android.tv.dvr.DvrManager;
@@ -47,47 +45,38 @@
 import com.android.tv.perf.PerformanceMonitor;
 import com.android.tv.perf.stub.StubPerformanceMonitor;
 import com.android.tv.testing.dvr.DvrDataManagerInMemoryImpl;
+import com.android.tv.testing.fakes.FakeClock;
 import com.android.tv.testing.testdata.TestData;
 import com.android.tv.tuner.singletons.TunerSingletons;
-import com.android.tv.tuner.source.TsDataSourceManager;
-import com.android.tv.tuner.source.TunerTsStreamerManager;
-import com.android.tv.tuner.tvinput.factory.TunerSessionFactory;
-import com.android.tv.tuner.tvinput.factory.TunerSessionFactoryImpl;
 import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
+import com.android.tv.util.AsyncDbTask.DbExecutor;
 import com.android.tv.util.SetupUtils;
 import com.android.tv.util.TvInputManagerHelper;
-import com.android.tv.util.account.AccountHelper;
-import com.google.common.base.Optional;
-import java.util.concurrent.Executor;
-import javax.inject.Provider;
 
-/** Test application for Live TV. */
+import com.google.common.base.Optional;
+
+import dagger.Lazy;
+
+import java.util.concurrent.Executor;
+
+/** Test application for TV app. */
 public class TestSingletonApp extends Application
         implements TvSingletons, TunerSingletons, HasSingletons<TvSingletons> {
     public final FakeClock fakeClock = FakeClock.createWithCurrentTime();
     public final FakeEpgReader epgReader = new FakeEpgReader(fakeClock);
     public final FakeEpgFetcher epgFetcher = new FakeEpgFetcher();
+    public final SettableFlagsModule flagsModule = new SettableFlagsModule();
 
     public FakeTvInputManagerHelper tvInputManagerHelper;
     public SetupUtils setupUtils;
     public DvrManager dvrManager;
     public DvrDataManager mDvrDataManager;
+    @DbExecutor public Executor dbExecutor = AsyncTask.SERIAL_EXECUTOR;
 
-    private final Provider<EpgReader> mEpgReaderProvider = SingletonProvider.create(epgReader);
+    private final Lazy<EpgReader> mEpgReaderProvider = () -> epgReader;
     private final Optional<BuiltInTunerManager> mBuiltInTunerManagerOptional = Optional.absent();
-    private final DefaultBackendKnobsFlags mBackendKnobs = new DefaultBackendKnobsFlags();
-    private final DefaultCloudEpgFlags mCloudEpgFlags = new DefaultCloudEpgFlags();
-    private final DefaultUiFlags mUiFlags = new DefaultUiFlags();
-    private final DefaultConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags =
-            new DefaultConcurrentDvrPlaybackFlags();
-    private final TsDataSourceManager.Factory mTsDataSourceManagerFactory =
-            new TsDataSourceManager.Factory(() -> new TunerTsStreamerManager(null));
-    private final TunerSessionFactoryImpl mTunerSessionFactory =
-            new TunerSessionFactoryImpl(
-                    new DefaultTunerFlags(),
-                    mConcurrentDvrPlaybackFlags,
-                    mTsDataSourceManagerFactory);
-    private PerformanceMonitor mPerformanceMonitor;
+
+    private final PerformanceMonitor mPerformanceMonitor = new StubPerformanceMonitor();
     private ChannelDataManager mChannelDataManager;
 
     @Override
@@ -96,7 +85,9 @@
         tvInputManagerHelper = new FakeTvInputManagerHelper(this);
         setupUtils = new SetupUtils(this, mBuiltInTunerManagerOptional);
         tvInputManagerHelper.start();
-        mChannelDataManager = new ChannelDataManager(this, tvInputManagerHelper);
+        mChannelDataManager =
+                new ChannelDataManager(
+                        this, tvInputManagerHelper, dbExecutor, getContentResolver());
         mChannelDataManager.start();
         mDvrDataManager = new DvrDataManagerInMemoryImpl(this, fakeClock);
         // HACK reset the singleton for tests
@@ -124,21 +115,11 @@
     }
 
     @Override
-    public boolean isChannelDataManagerLoadFinished() {
-        return false;
-    }
-
-    @Override
     public ProgramDataManager getProgramDataManager() {
         return null;
     }
 
     @Override
-    public boolean isProgramDataManagerCurrentProgramsLoadFinished() {
-        return false;
-    }
-
-    @Override
     public PreviewDataManager getPreviewDataManager() {
         return null;
     }
@@ -184,16 +165,11 @@
     }
 
     @Override
-    public Provider<EpgReader> providesEpgReader() {
+    public Lazy<EpgReader> providesEpgReader() {
         return mEpgReaderProvider;
     }
 
     @Override
-    public EpgFetcher getEpgFetcher() {
-        return epgFetcher;
-    }
-
-    @Override
     public SetupUtils getSetupUtils() {
         return setupUtils;
     }
@@ -204,21 +180,11 @@
     }
 
     @Override
-    public ExperimentLoader getExperimentLoader() {
-        return new ExperimentLoader();
-    }
-
-    @Override
     public MainActivityWrapper getMainActivityWrapper() {
         return null;
     }
 
     @Override
-    public AccountHelper getAccountHelper() {
-        return null;
-    }
-
-    @Override
     public Clock getClock() {
         return fakeClock;
     }
@@ -235,9 +201,6 @@
 
     @Override
     public PerformanceMonitor getPerformanceMonitor() {
-        if (mPerformanceMonitor == null) {
-            mPerformanceMonitor = new StubPerformanceMonitor();
-        }
         return mPerformanceMonitor;
     }
 
@@ -248,22 +211,22 @@
 
     @Override
     public Executor getDbExecutor() {
-        return AsyncTask.SERIAL_EXECUTOR;
+        return dbExecutor;
     }
 
     @Override
     public DefaultBackendKnobsFlags getBackendKnobs() {
-        return mBackendKnobs;
+        return flagsModule.backendKnobsFlags;
     }
 
     @Override
     public DefaultCloudEpgFlags getCloudEpgFlags() {
-        return mCloudEpgFlags;
+        return flagsModule.cloudEpgFlags;
     }
 
     @Override
     public DefaultUiFlags getUiFlags() {
-        return mUiFlags;
+        return flagsModule.uiFlags;
     }
 
     @Override
@@ -272,15 +235,6 @@
     }
 
     @Override
-    public DefaultConcurrentDvrPlaybackFlags getConcurrentDvrPlaybackFlags() {
-        return mConcurrentDvrPlaybackFlags;
-    }
-
-    public TunerSessionFactory getTunerSessionFactory() {
-        return mTunerSessionFactory;
-    }
-
-    @Override
     public TvSingletons singletons() {
         return this;
     }
diff --git a/tests/common/src/com/android/tv/testing/FakeClock.java b/tests/common/src/com/android/tv/testing/fakes/FakeClock.java
similarity index 98%
rename from tests/common/src/com/android/tv/testing/FakeClock.java
rename to tests/common/src/com/android/tv/testing/fakes/FakeClock.java
index f594193..adef3cd 100644
--- a/tests/common/src/com/android/tv/testing/FakeClock.java
+++ b/tests/common/src/com/android/tv/testing/fakes/FakeClock.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.tv.testing;
+package com.android.tv.testing.fakes;
 
 import com.android.tv.common.util.Clock;
 import java.util.concurrent.TimeUnit;
diff --git a/tests/common/src/com/android/tv/testing/FakeTvProvider.java b/tests/common/src/com/android/tv/testing/fakes/FakeTvProvider.java
similarity index 99%
rename from tests/common/src/com/android/tv/testing/FakeTvProvider.java
rename to tests/common/src/com/android/tv/testing/fakes/FakeTvProvider.java
index 20903c6..36e97bc 100644
--- a/tests/common/src/com/android/tv/testing/FakeTvProvider.java
+++ b/tests/common/src/com/android/tv/testing/fakes/FakeTvProvider.java
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.tv.testing;
+package com.android.tv.testing.fakes;
 
 import android.annotation.SuppressLint;
 import android.content.ContentProvider;
@@ -54,7 +54,7 @@
 import androidx.tvprovider.media.tv.TvContractCompat.Programs.Genres;
 import androidx.tvprovider.media.tv.TvContractCompat.RecordedPrograms;
 import androidx.tvprovider.media.tv.TvContractCompat.WatchNextPrograms;
-import com.android.tv.util.SqlParams;
+import com.android.tv.common.util.sql.SqlParams;
 import java.io.ByteArrayOutputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
diff --git a/tests/common/src/com/android/tv/testing/robo/ContentProviders.java b/tests/common/src/com/android/tv/testing/robo/ContentProviders.java
new file mode 100644
index 0000000..aaaa11d
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/robo/ContentProviders.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.testing.robo;
+
+import android.content.ContentProvider;
+import android.content.pm.ProviderInfo;
+import org.robolectric.Robolectric;
+import org.robolectric.android.controller.ContentProviderController;
+import org.robolectric.shadows.ShadowContentResolver;
+
+/** Static utilities for using content providers in tests. */
+public final class ContentProviders {
+
+    /** Builds creates and register a ContentProvider with the given authority. */
+    public static <T extends ContentProvider> T register(Class<T> providerClass, String authority) {
+        ProviderInfo info = new ProviderInfo();
+        info.authority = authority;
+        ContentProviderController<T> contentProviderController =
+                Robolectric.buildContentProvider(providerClass);
+        T provider = contentProviderController.create(info).get();
+        provider.onCreate();
+        ShadowContentResolver.registerProviderInternal(authority, provider);
+        return provider;
+    }
+
+    private ContentProviders() {}
+}
diff --git a/tests/common/src/com/android/tv/testing/robo/RobotTestAppHelper.java b/tests/common/src/com/android/tv/testing/robo/RobotTestAppHelper.java
new file mode 100644
index 0000000..ad91f3d
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/robo/RobotTestAppHelper.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tv.testing.robo;
+
+import android.media.tv.TvContract;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.fakes.FakeTvProvider;
+import com.android.tv.testing.testdata.TestData;
+import java.util.concurrent.TimeUnit;
+import org.robolectric.Robolectric;
+
+/** Static utilities for using {@link TestSingletonApp} in roboletric tests. */
+public final class RobotTestAppHelper {
+
+    public static void loadTestData(TestSingletonApp app, TestData testData) {
+        ContentProviders.register(FakeTvProvider.class, TvContract.AUTHORITY);
+        app.loadTestData(testData, TimeUnit.DAYS.toMillis(1));
+        Robolectric.flushBackgroundThreadScheduler();
+        Robolectric.flushForegroundThreadScheduler();
+    }
+
+    private RobotTestAppHelper() {}
+}
diff --git a/tests/common/src/com/android/tv/testing/shadows/ShadowMediaSession.java b/tests/common/src/com/android/tv/testing/shadows/ShadowMediaSession.java
new file mode 100644
index 0000000..5a2c41e
--- /dev/null
+++ b/tests/common/src/com/android/tv/testing/shadows/ShadowMediaSession.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.testing.shadows;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.media.MediaMetadata;
+import android.media.session.MediaSession;
+import android.media.session.PlaybackState;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+/** Shadow {@link MediaSession}. */
+@Implements(MediaSession.class)
+public class ShadowMediaSession {
+
+    public MediaSession.Callback mCallback;
+    public PendingIntent mMediaButtonReceiver;
+    public PendingIntent mSessionActivity;
+    public PlaybackState mPlaybackState;
+    public MediaMetadata mMediaMetadata;
+    public int mFlags;
+    public boolean mActive;
+    public boolean mReleased;
+
+    /** Stand-in for the MediaSession constructor with the same parameters. */
+    public void __constructor__(Context context, String tag, int userID) {
+        // This empty method prevents the real MediaSession constructor from being called.
+    }
+
+    @Implementation
+    public void setCallback(MediaSession.Callback callback) {
+        mCallback = callback;
+    }
+
+    @Implementation
+    public void setMediaButtonReceiver(PendingIntent mbr) {
+        mMediaButtonReceiver = mbr;
+    }
+
+    @Implementation
+    public void setSessionActivity(PendingIntent activity) {
+        mSessionActivity = activity;
+    }
+
+    @Implementation
+    public void setPlaybackState(PlaybackState state) {
+        mPlaybackState = state;
+    }
+
+    @Implementation
+    public void setMetadata(MediaMetadata metadata) {
+        mMediaMetadata = metadata;
+    }
+
+    @Implementation
+    public void setFlags(int flags) {
+        mFlags = flags;
+    }
+
+    @Implementation
+    public boolean isActive() {
+        return mActive;
+    }
+
+    @Implementation
+    public void setActive(boolean active) {
+        mActive = active;
+    }
+
+    @Implementation
+    public void release() {
+        mReleased = true;
+    }
+}
diff --git a/tests/common/src/com/android/tv/testing/uihelper/LiveChannelsUiDeviceHelper.java b/tests/common/src/com/android/tv/testing/uihelper/LiveChannelsUiDeviceHelper.java
index 4b7c1f8..30fbf37 100644
--- a/tests/common/src/com/android/tv/testing/uihelper/LiveChannelsUiDeviceHelper.java
+++ b/tests/common/src/com/android/tv/testing/uihelper/LiveChannelsUiDeviceHelper.java
@@ -30,7 +30,7 @@
 import com.android.tv.testing.utils.Utils;
 import junit.framework.Assert;
 
-/** Helper for testing the Live TV Application. */
+/** Helper for testing the TV application. */
 public class LiveChannelsUiDeviceHelper extends BaseUiDeviceHelper {
     private static final String TAG = "LiveChannelsUiDevice";
     private static final int APPLICATION_START_TIMEOUT_MSEC = 5000;
@@ -56,7 +56,7 @@
         waitForCondition(mUiDevice, Until.hasObject(Constants.TV_VIEW));
 
         Assert.assertTrue(
-            Constants.TV_APP_PACKAGE + " did not start",
+                Constants.TV_APP_PACKAGE + " did not start",
                 mUiDevice.wait(
                         Until.hasObject(By.pkg(Constants.TV_APP_PACKAGE).depth(0)),
                         APPLICATION_START_TIMEOUT_MSEC));
diff --git a/tests/func/AndroidManifest.xml b/tests/func/AndroidManifest.xml
index 3d7d775..e60773f 100644
--- a/tests/func/AndroidManifest.xml
+++ b/tests/func/AndroidManifest.xml
@@ -18,7 +18,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.tv.tests.ui" >
 
-    <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23" />
+    <uses-sdk android:targetSdkVersion="28" android:minSdkVersion="23" />
 
     <instrumentation
         android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/tests/func/src/com/android/tv/tests/ui/PlayControlsRowViewTest.java b/tests/func/src/com/android/tv/tests/ui/PlayControlsRowViewTest.java
index efc7ecf..e24c72f 100644
--- a/tests/func/src/com/android/tv/tests/ui/PlayControlsRowViewTest.java
+++ b/tests/func/src/com/android/tv/tests/ui/PlayControlsRowViewTest.java
@@ -135,7 +135,7 @@
         controller.pressKeyCode(KeyEvent.KEYCODE_MEDIA_PAUSE);
         controller.menuHelper.assertWaitForMenu();
         assertButtonHasFocus(BUTTON_ID_PLAY_PAUSE);
-        // Press HOME twice to visit the home screen and return to Live TV.
+        // Press HOME twice to visit the home screen and return to TV app.
         controller.pressHome();
         // Wait until home screen is shown.
         controller.waitForIdle();
diff --git a/tests/input/AndroidManifest.xml b/tests/input/AndroidManifest.xml
index fa52946..564323a 100644
--- a/tests/input/AndroidManifest.xml
+++ b/tests/input/AndroidManifest.xml
@@ -18,7 +18,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.tv.testinput">
 
-    <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
+    <uses-sdk android:targetSdkVersion="28" android:minSdkVersion="23"/>
 
      <!-- Required to update or read existing channel and program information in TvProvider. -->
     <uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" />
diff --git a/tests/input/src/com/android/tv/testinput/TestTvInputService.java b/tests/input/src/com/android/tv/testinput/TestTvInputService.java
index 840587c..d19f455 100644
--- a/tests/input/src/com/android/tv/testinput/TestTvInputService.java
+++ b/tests/input/src/com/android/tv/testinput/TestTvInputService.java
@@ -53,7 +53,7 @@
     private static final int REFRESH_DELAY_MS = 1000 / 5;
     private static final boolean DEBUG = false;
 
-    // Consider the command delivering time from Live TV.
+    // Consider the command delivering time from TV app.
     private static final long MAX_COMMAND_DELAY = TimeUnit.SECONDS.toMillis(3);
 
     private final TestInputControl mBackend = TestInputControl.getInstance();
diff --git a/tests/jank/AndroidManifest.xml b/tests/jank/AndroidManifest.xml
index 7c0997a..1538851 100644
--- a/tests/jank/AndroidManifest.xml
+++ b/tests/jank/AndroidManifest.xml
@@ -18,7 +18,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.tv.tests.jank" >
 
-    <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23" />
+    <uses-sdk android:targetSdkVersion="28" android:minSdkVersion="23" />
 
     <instrumentation
             android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/tests/jank/src/com/android/tv/tests/jank/Utils.java b/tests/jank/src/com/android/tv/tests/jank/Utils.java
index 4ad0f64..57e5f10 100644
--- a/tests/jank/src/com/android/tv/tests/jank/Utils.java
+++ b/tests/jank/src/com/android/tv/tests/jank/Utils.java
@@ -19,7 +19,7 @@
 import com.android.tv.testing.uihelper.UiDeviceUtils;
 
 public final class Utils {
-    /** Live TV process name */
+    /** TV app process name */
     public static final String LIVE_CHANNELS_PROCESS_NAME = "com.android.tv";
 
     private Utils() {}
diff --git a/tests/robotests/README.md b/tests/robotests/README.md
new file mode 100644
index 0000000..8e4bcb5
--- /dev/null
+++ b/tests/robotests/README.md
@@ -0,0 +1,5 @@
+Unit test suite for Live Channels using Robolectric.
+
+```
+$ m -j96 RunTvRoboTests
+```
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/tv/MainActivityRoboTest.java b/tests/robotests/src/com/android/tv/MainActivityRoboTest.java
new file mode 100644
index 0000000..357c163
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/MainActivityRoboTest.java
@@ -0,0 +1,169 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.media.tv.TvTrackInfo;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import com.android.tv.common.flags.impl.DefaultLegacyFlags;
+import com.android.tv.data.ProgramDataManager;
+import com.android.tv.data.StreamInfo;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.android.tv.ui.TunableTvView;
+import com.android.tv.util.TvInputManagerHelper;
+import java.util.Arrays;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+
+/** Tests for {@link TunableTvView} */
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        sdk = ConfigConstants.SDK,
+        application = TestSingletonApp.class,
+        shadows = {ShadowTvView.class})
+public class MainActivityRoboTest {
+    private ShadowTvView mShadowTvView;
+    private FakeMainActivity mMainActivity;
+
+    @Before
+    public void setUp() {
+        mMainActivity = Robolectric.buildActivity(FakeMainActivity.class).create().get();
+        mShadowTvView = Shadow.extract(mMainActivity.getTvView().getTvView());
+        mShadowTvView.listener = mMainActivity.getListener();
+    }
+
+    @Test
+    public void testSelectAudioTrack_autoSelect() {
+        mShadowTvView.mAudioTrackCountChanged = false;
+        setTracks(
+                TvTrackInfo.TYPE_AUDIO,
+                buildTrackForTesting(TvTrackInfo.TYPE_AUDIO, "EN audio 1", "EN"),
+                buildTrackForTesting(TvTrackInfo.TYPE_AUDIO, "FR audio 1", "FR"));
+        mMainActivity.selectAudioTrack("FR audio 1");
+        assertThat(mMainActivity.getSelectedTrack(TvTrackInfo.TYPE_AUDIO)).isEqualTo("FR audio 1");
+
+        setTracks(
+                TvTrackInfo.TYPE_AUDIO,
+                buildTrackForTesting(TvTrackInfo.TYPE_AUDIO, "EN audio 2", "EN"),
+                buildTrackForTesting(TvTrackInfo.TYPE_AUDIO, "FR audio 2", "FR"),
+                buildTrackForTesting(TvTrackInfo.TYPE_AUDIO, "FR audio 3", "FR"));
+        mMainActivity.applyMultiAudio(null);
+        // FR audio 2 is selected according the previously selected track.
+        assertThat(mMainActivity.getSelectedTrack(TvTrackInfo.TYPE_AUDIO)).isEqualTo("FR audio 2");
+    }
+
+    @Test
+    public void testSelectAudioTrack_audioTrackCountChanged() {
+        mShadowTvView.mAudioTrackCountChanged = true;
+        setTracks(
+                TvTrackInfo.TYPE_AUDIO,
+                buildTrackForTesting(TvTrackInfo.TYPE_AUDIO, "EN audio 1", "EN"),
+                buildTrackForTesting(TvTrackInfo.TYPE_AUDIO, "FR audio 1", "FR"));
+        mMainActivity.selectAudioTrack("FR audio 1");
+        assertThat(mMainActivity.getSelectedTrack(TvTrackInfo.TYPE_AUDIO)).isEqualTo("FR audio 1");
+
+        setTracks(
+                TvTrackInfo.TYPE_AUDIO,
+                buildTrackForTesting(TvTrackInfo.TYPE_AUDIO, "EN audio 2", "EN"),
+                buildTrackForTesting(TvTrackInfo.TYPE_AUDIO, "FR audio 2", "FR"),
+                buildTrackForTesting(TvTrackInfo.TYPE_AUDIO, "FR audio 3", "FR"));
+        mMainActivity.selectAudioTrack("FR audio 3");
+        // FR audio 3 is selected even if the track info has been changed
+        assertThat(mMainActivity.getSelectedTrack(TvTrackInfo.TYPE_AUDIO)).isEqualTo("FR audio 3");
+    }
+
+    @Test
+    public void testSelectAudioTrack_audioTrackCountNotChanged() {
+        mShadowTvView.mAudioTrackCountChanged = false;
+        setTracks(
+                TvTrackInfo.TYPE_AUDIO,
+                buildTrackForTesting(TvTrackInfo.TYPE_AUDIO, "EN audio 1", "EN"),
+                buildTrackForTesting(TvTrackInfo.TYPE_AUDIO, "FR audio 1", "FR"));
+        mMainActivity.selectAudioTrack("FR audio 1");
+        assertThat(mMainActivity.getSelectedTrack(TvTrackInfo.TYPE_AUDIO)).isEqualTo("FR audio 1");
+
+        setTracks(
+                TvTrackInfo.TYPE_AUDIO,
+                buildTrackForTesting(TvTrackInfo.TYPE_AUDIO, "EN audio 2", "EN"),
+                buildTrackForTesting(TvTrackInfo.TYPE_AUDIO, "FR audio 2", "FR"),
+                buildTrackForTesting(TvTrackInfo.TYPE_AUDIO, "FR audio 3", "FR"));
+        mMainActivity.selectAudioTrack("FR audio 3");
+        assertThat(mMainActivity.getSelectedTrack(TvTrackInfo.TYPE_AUDIO)).isEqualTo("FR audio 3");
+    }
+
+    private void setTracks(int type, TvTrackInfo... tracks) {
+        mShadowTvView.mTracks.put(type, Arrays.asList(tracks));
+        mShadowTvView.mSelectedTracks.put(type, null);
+    }
+
+    private TvTrackInfo buildTrackForTesting(int type, String id, String language) {
+        return new TvTrackInfo.Builder(type, id)
+                .setLanguage(language)
+                .setAudioChannelCount(0)
+                .build();
+    }
+
+    /** A {@link MainActivity} class for tests */
+    public static class FakeMainActivity extends MainActivity {
+        private MyOnTuneListener mListener;
+
+        @Override
+        protected void onCreate(Bundle savedInstanceState) {
+            // Override onCreate() to omit unnecessary member variables
+            mTvView =
+                    (TunableTvView)
+                            LayoutInflater.from(RuntimeEnvironment.application)
+                                    .inflate(R.layout.activity_tv, null)
+                                    .findViewById(R.id.main_tunable_tv_view);
+            DefaultLegacyFlags legacyFlags = DefaultLegacyFlags.DEFAULT;
+            mTvView.initialize(
+                    new ProgramDataManager(RuntimeEnvironment.application),
+                    new TvInputManagerHelper(RuntimeEnvironment.application, legacyFlags),
+                    legacyFlags);
+            mTvView.start();
+            mListener =
+                    new MyOnTuneListener() {
+                        @Override
+                        public void onStreamInfoChanged(
+                                StreamInfo info, boolean allowAutoSelectionOfTrack) {
+                            applyMultiAudio(
+                                    allowAutoSelectionOfTrack
+                                            ? null
+                                            : getSelectedTrack(TvTrackInfo.TYPE_AUDIO));
+                        }
+                    };
+            mTvView.setOnTuneListener(mListener);
+        }
+
+        public TunableTvView getTvView() {
+            return mTvView;
+        }
+
+        public MyOnTuneListener getListener() {
+            return mListener;
+        }
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/MediaSessionWrapperTest.java b/tests/robotests/src/com/android/tv/MediaSessionWrapperTest.java
new file mode 100644
index 0000000..1a2b2ca
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/MediaSessionWrapperTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.media.MediaMetadata;
+import android.media.session.PlaybackState;
+import com.android.tv.testing.EpgTestData;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.android.tv.testing.shadows.ShadowMediaSession;
+import com.google.common.collect.Maps;
+import java.util.Map;
+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;
+import org.robolectric.shadow.api.Shadow;
+
+/** Tests fpr {@link MediaSessionWrapper}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(
+    sdk = ConfigConstants.SDK,
+    application = TestSingletonApp.class,
+    shadows = {ShadowMediaSession.class}
+)
+public class MediaSessionWrapperTest {
+
+    private static final int TEST_REQUEST_CODE = 1337;
+
+    private ShadowMediaSession mediaSession;
+    private MediaSessionWrapper mediaSessionWrapper;
+    private PendingIntent pendingIntent;
+
+    @Before
+    public void setUp() {
+        pendingIntent =
+                PendingIntent.getActivity(
+                        RuntimeEnvironment.application, TEST_REQUEST_CODE, new Intent(), 0);
+        mediaSessionWrapper =
+                new MediaSessionWrapper(RuntimeEnvironment.application, pendingIntent) {
+                    @Override
+                    void initMediaController() {
+                        // Not use MediaController for tests here because:
+                        // 1. it's not allow to shadow MediaController
+                        // 2. The Context TestSingletonApp is not an instance of Activity so
+                        // Activity.setMediaController cannot be called.
+                        // onPlaybackStateChanged() is called in #setPlaybackState instead.
+                    }
+                    @Override
+                    void unregisterMediaControllerCallback() {
+                    }
+                };
+        mediaSession = Shadow.extract(mediaSessionWrapper.getMediaSession());
+    }
+
+    @Test
+    public void setSessionActivity() {
+        assertThat(mediaSession.mSessionActivity).isEqualTo(this.pendingIntent);
+    }
+
+    @Test
+    public void setPlaybackState_true() {
+        setPlaybackState(true);
+        assertThat(mediaSession.mActive).isTrue();
+        assertThat(mediaSession.mPlaybackState.getState()).isEqualTo(PlaybackState.STATE_PLAYING);
+    }
+
+    @Test
+    public void setPlaybackState_false() {
+        setPlaybackState(false);
+        assertThat(mediaSession.mActive).isFalse();
+        assertThat(mediaSession.mPlaybackState).isNull();
+    }
+
+    @Test
+    public void setPlaybackState_trueThenFalse() {
+        setPlaybackState(true);
+        setPlaybackState(false);
+        assertThat(mediaSession.mActive).isFalse();
+        assertThat(mediaSession.mPlaybackState.getState()).isEqualTo(PlaybackState.STATE_STOPPED);
+    }
+
+    @Test
+    public void update_channel10() {
+
+        mediaSessionWrapper.update(false, EpgTestData.toTvChannel(EpgTestData.CHANNEL_10), null);
+        assertThat(asMap(mediaSession.mMediaMetadata))
+                .containsExactly(MediaMetadata.METADATA_KEY_TITLE, "Channel TEN");
+    }
+
+    @Test
+    public void update_blockedChannel10() {
+        mediaSessionWrapper.update(true, EpgTestData.toTvChannel(EpgTestData.CHANNEL_10), null);
+        assertThat(asMap(mediaSession.mMediaMetadata))
+                .containsExactly(
+                        MediaMetadata.METADATA_KEY_TITLE,
+                        "Channel blocked",
+                        MediaMetadata.METADATA_KEY_ART,
+                        null);
+    }
+
+    @Test
+    public void update_channel10Program2() {
+        mediaSessionWrapper.update(
+                false, EpgTestData.toTvChannel(EpgTestData.CHANNEL_10), EpgTestData.PROGRAM_2);
+        assertThat(asMap(mediaSession.mMediaMetadata))
+                .containsExactly(MediaMetadata.METADATA_KEY_TITLE, "Program 2");
+    }
+
+    @Test
+    public void update_blockedChannel10Program2() {
+        mediaSessionWrapper.update(
+                true, EpgTestData.toTvChannel(EpgTestData.CHANNEL_10), EpgTestData.PROGRAM_2);
+        assertThat(asMap(mediaSession.mMediaMetadata))
+                .containsExactly(
+                        MediaMetadata.METADATA_KEY_TITLE,
+                        "Channel blocked",
+                        MediaMetadata.METADATA_KEY_ART,
+                        null);
+        // TODO(b/70559407): test async loading of images.
+    }
+
+    @Test
+    public void release() {
+        mediaSessionWrapper.release();
+        assertThat(mediaSession.mReleased).isTrue();
+    }
+
+    private Map<String, Object> asMap(MediaMetadata mediaMetadata) {
+        return Maps.asMap(mediaMetadata.keySet(), key -> mediaMetadata.getString(key));
+    }
+
+    private void setPlaybackState(boolean isPlaying) {
+        mediaSessionWrapper.setPlaybackState(isPlaying);
+        mediaSessionWrapper.getMediaControllerCallback().onPlaybackStateChanged(
+                isPlaying
+                        ? MediaSessionWrapper.MEDIA_SESSION_STATE_PLAYING
+                        : MediaSessionWrapper.MEDIA_SESSION_STATE_STOPPED);
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/SetupPassthroughActivityTest.java b/tests/robotests/src/com/android/tv/SetupPassthroughActivityTest.java
new file mode 100644
index 0000000..a442ec1
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/SetupPassthroughActivityTest.java
@@ -0,0 +1,446 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import static org.robolectric.Shadows.shadowOf;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ServiceInfo;
+import android.media.tv.TvInputInfo;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.Nullable;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.tv.common.CommonConstants;
+import com.android.tv.common.dagger.ApplicationModule;
+import com.android.tv.common.flags.impl.DefaultLegacyFlags;
+import com.android.tv.common.flags.impl.SettableFlagsModule;
+import com.android.tv.common.util.CommonUtils;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.epg.EpgFetcher;
+import com.android.tv.features.TvFeatures;
+import com.android.tv.modules.TvSingletonsModule;
+import com.android.tv.testing.FakeTvInputManager;
+import com.android.tv.testing.FakeTvInputManagerHelper;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
+import com.android.tv.util.AsyncDbTask.DbExecutor;
+import com.android.tv.util.SetupUtils;
+import com.android.tv.util.TvInputManagerHelper;
+
+import com.google.android.tv.partner.support.EpgContract;
+import com.google.common.base.Optional;
+
+import dagger.Component;
+import dagger.Module;
+import dagger.Provides;
+import dagger.android.AndroidInjectionModule;
+import dagger.android.AndroidInjector;
+import dagger.android.DispatchingAndroidInjector;
+import dagger.android.HasAndroidInjector;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mockito;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.android.util.concurrent.RoboExecutorService;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowActivity;
+
+import java.util.concurrent.Executor;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/** Tests for {@link SetupPassthroughActivity}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK, application = SetupPassthroughActivityTest.MyTestApp.class)
+public class SetupPassthroughActivityTest {
+
+    private static final int REQUEST_START_SETUP_ACTIVITY = 200;
+
+    private MyTestApp testSingletonApp;
+
+    private TvInputInfo testInput;
+
+    @Before
+    public void setup() {
+        testInput = createMockInput("com.example/.Input");
+        testSingletonApp = (MyTestApp) ApplicationProvider.getApplicationContext();
+        testSingletonApp.flagsModule.legacyFlags =
+                DefaultLegacyFlags.builder().enableQaFeatures(true).build();
+        testSingletonApp.tvInputManagerHelper.getFakeTvInputManager();
+    }
+
+    @After
+    public void after() {
+        TvFeatures.CLOUD_EPG_FOR_3RD_PARTY.resetForTests();
+    }
+
+    @Test
+    public void create_emptyIntent() {
+        SetupPassthroughActivity activity =
+                Robolectric.buildActivity(SetupPassthroughActivity.class, new Intent())
+                        .create()
+                        .get();
+        ShadowActivity.IntentForResult shadowIntent =
+                shadowOf(activity).getNextStartedActivityForResult();
+        // Since there is no inputs, the next activity should not be started.
+        assertThat(shadowIntent).isNull();
+        assertThat(activity.isFinishing()).isTrue();
+    }
+
+    @Test
+    public void create_noInputs() {
+        SetupPassthroughActivity activity = createSetupActivityFor("com.example/.Input");
+        ShadowActivity.IntentForResult shadowIntent =
+                shadowOf(activity).getNextStartedActivityForResult();
+        // Since there is no inputs, the next activity should not be started.
+        assertThat(shadowIntent).isNull();
+        assertThat(activity.isFinishing()).isTrue();
+    }
+
+    @Test
+    public void create_inputNotFound() {
+        testSingletonApp.tvInputManagerHelper = new FakeTvInputManagerHelper(testSingletonApp);
+        testSingletonApp.tvInputManagerHelper.start();
+        testSingletonApp.tvInputManagerHelper.getFakeTvInputManager().add(testInput, -1);
+        SetupPassthroughActivity activity = createSetupActivityFor("com.example/.Other");
+        ShadowActivity.IntentForResult shadowIntent =
+                shadowOf(activity).getNextStartedActivityForResult();
+        // Since the input is not found, the next activity should not be started.
+        assertThat(shadowIntent).isNull();
+        assertThat(activity.isFinishing()).isTrue();
+    }
+
+    @Test
+    public void create_validInput() {
+        testSingletonApp.tvInputManagerHelper.start();
+        testSingletonApp.tvInputManagerHelper.getFakeTvInputManager().add(testInput, -1);
+        SetupPassthroughActivity activity = createSetupActivityFor(testInput.getId());
+
+        ShadowActivity.IntentForResult shadowIntent =
+                shadowOf(activity).getNextStartedActivityForResult();
+        assertThat(shadowIntent).isNotNull();
+        assertThat(shadowIntent.options).isNull();
+        assertThat(shadowIntent.intent.getExtras()).isNotNull();
+        assertThat(shadowIntent.requestCode).isEqualTo(REQUEST_START_SETUP_ACTIVITY);
+        assertThat(activity.isFinishing()).isFalse();
+    }
+
+    @Test
+    public void create_trustedCallingPackage() {
+        testSingletonApp.tvInputManagerHelper.start();
+        testSingletonApp.tvInputManagerHelper.getFakeTvInputManager().add(testInput, -1);
+
+        ActivityController<SetupPassthroughActivity> activityController =
+                Robolectric.buildActivity(
+                        SetupPassthroughActivity.class,
+                        CommonUtils.createSetupIntent(new Intent(), testInput.getId()));
+        SetupPassthroughActivity activity = activityController.get();
+        ShadowActivity shadowActivity = shadowOf(activity);
+        shadowActivity.setCallingActivity(
+                new ComponentName(CommonConstants.BASE_PACKAGE, "com.example.MyClass"));
+        activityController.create();
+
+        ShadowActivity.IntentForResult shadowIntent =
+                shadowActivity.getNextStartedActivityForResult();
+        assertThat(shadowIntent).isNotNull();
+        assertThat(shadowIntent.options).isNull();
+        assertThat(shadowIntent.intent.getExtras()).isNotNull();
+        assertThat(shadowIntent.requestCode).isEqualTo(REQUEST_START_SETUP_ACTIVITY);
+        assertThat(activity.isFinishing()).isFalse();
+    }
+
+    @Test
+    public void create_nonTrustedCallingPackage() {
+        testSingletonApp.tvInputManagerHelper.start();
+        testSingletonApp.tvInputManagerHelper.getFakeTvInputManager().add(testInput, -1);
+
+        ActivityController<SetupPassthroughActivity> activityController =
+                Robolectric.buildActivity(
+                        SetupPassthroughActivity.class,
+                        CommonUtils.createSetupIntent(new Intent(), testInput.getId()));
+        SetupPassthroughActivity activity = activityController.get();
+        ShadowActivity shadowActivity = shadowOf(activity);
+        shadowActivity.setCallingActivity(
+                new ComponentName("com.notTrusted", "com.notTrusted.MyClass"));
+        activityController.create();
+
+        ShadowActivity.IntentForResult shadowIntent =
+                shadowActivity.getNextStartedActivityForResult();
+        // Since the calling activity is not trusted, the next activity should not be started.
+        assertThat(shadowIntent).isNull();
+        assertThat(activity.isFinishing()).isTrue();
+    }
+
+    @Test
+    public void onActivityResult_canceled() {
+        testSingletonApp.tvInputManagerHelper.getFakeTvInputManager().add(testInput, -1);
+        SetupPassthroughActivity activity = createSetupActivityFor(testInput.getId());
+
+        activity.onActivityResult(0, Activity.RESULT_CANCELED, null);
+        assertThat(activity.isFinishing()).isTrue();
+        assertThat(shadowOf(activity).getResultCode()).isEqualTo(Activity.RESULT_CANCELED);
+    }
+
+    @Test
+    public void onActivityResult_ok() {
+        TestSetupUtils setupUtils = new TestSetupUtils(RuntimeEnvironment.application);
+        testSingletonApp.setupUtils = setupUtils;
+        testSingletonApp.tvInputManagerHelper.getFakeTvInputManager().add(testInput, -1);
+        SetupPassthroughActivity activity = createSetupActivityFor(testInput.getId());
+        activity.onActivityResult(REQUEST_START_SETUP_ACTIVITY, Activity.RESULT_OK, null);
+
+        assertThat(testSingletonApp.epgFetcher.fetchStarted).isFalse();
+        assertThat(setupUtils.finishedId).isEqualTo("com.example/.Input");
+        assertThat(activity.isFinishing()).isFalse();
+
+        setupUtils.finishedRunnable.run();
+        assertThat(activity.isFinishing()).isTrue();
+        assertThat(shadowOf(activity).getResultCode()).isEqualTo(Activity.RESULT_OK);
+    }
+
+    @Test
+    public void onActivityResult_3rdPartyEpg_ok() {
+        TvFeatures.CLOUD_EPG_FOR_3RD_PARTY.enableForTest();
+        TestSetupUtils setupUtils = new TestSetupUtils(RuntimeEnvironment.application);
+        testSingletonApp.setupUtils = setupUtils;
+        testSingletonApp.tvInputManagerHelper.getFakeTvInputManager().add(testInput, -1);
+        testSingletonApp.getCloudEpgFlags().setThirdPartyEpgInputCsv(testInput.getId());
+        SetupPassthroughActivity activity = createSetupActivityFor(testInput.getId());
+        Intent data = new Intent();
+        data.putExtra(EpgContract.EXTRA_USE_CLOUD_EPG, true);
+        data.putExtra(TvInputInfo.EXTRA_INPUT_ID, testInput.getId());
+        activity.onActivityResult(REQUEST_START_SETUP_ACTIVITY, Activity.RESULT_OK, data);
+
+        assertThat(testSingletonApp.epgFetcher.fetchStarted).isTrue();
+        assertThat(setupUtils.finishedId).isEqualTo("com.example/.Input");
+        assertThat(activity.isFinishing()).isFalse();
+
+        setupUtils.finishedRunnable.run();
+        assertThat(activity.isFinishing()).isTrue();
+        assertThat(shadowOf(activity).getResultCode()).isEqualTo(Activity.RESULT_OK);
+    }
+
+    @Test
+    public void onActivityResult_3rdPartyEpg_notWhiteListed() {
+        TvFeatures.CLOUD_EPG_FOR_3RD_PARTY.enableForTest();
+        TestSetupUtils setupUtils = new TestSetupUtils(RuntimeEnvironment.application);
+        testSingletonApp.setupUtils = setupUtils;
+        testSingletonApp.tvInputManagerHelper.getFakeTvInputManager().add(testInput, -1);
+        SetupPassthroughActivity activity = createSetupActivityFor(testInput.getId());
+        Intent data = new Intent();
+        data.putExtra(EpgContract.EXTRA_USE_CLOUD_EPG, true);
+        data.putExtra(TvInputInfo.EXTRA_INPUT_ID, testInput.getId());
+        activity.onActivityResult(REQUEST_START_SETUP_ACTIVITY, Activity.RESULT_OK, data);
+
+        assertThat(testSingletonApp.epgFetcher.fetchStarted).isFalse();
+        assertThat(setupUtils.finishedId).isEqualTo("com.example/.Input");
+        assertThat(activity.isFinishing()).isFalse();
+
+        setupUtils.finishedRunnable.run();
+        assertThat(activity.isFinishing()).isTrue();
+        assertThat(shadowOf(activity).getResultCode()).isEqualTo(Activity.RESULT_OK);
+    }
+
+    @Test
+    public void onActivityResult_3rdPartyEpg_disabled() {
+        TvFeatures.CLOUD_EPG_FOR_3RD_PARTY.disableForTests();
+        TestSetupUtils setupUtils = new TestSetupUtils(RuntimeEnvironment.application);
+        testSingletonApp.setupUtils = setupUtils;
+        testSingletonApp.tvInputManagerHelper.getFakeTvInputManager().add(testInput, -1);
+        testSingletonApp.getCloudEpgFlags().setThirdPartyEpgInputCsv(testInput.getId());
+        testSingletonApp.dbExecutor = new RoboExecutorService();
+        SetupPassthroughActivity activity = createSetupActivityFor(testInput.getId());
+        Intent data = new Intent();
+        data.putExtra(EpgContract.EXTRA_USE_CLOUD_EPG, true);
+        data.putExtra(TvInputInfo.EXTRA_INPUT_ID, testInput.getId());
+        activity.onActivityResult(REQUEST_START_SETUP_ACTIVITY, Activity.RESULT_OK, data);
+
+        assertThat(testSingletonApp.epgFetcher.fetchStarted).isFalse();
+        assertThat(setupUtils.finishedId).isEqualTo("com.example/.Input");
+        assertThat(activity.isFinishing()).isFalse();
+
+        setupUtils.finishedRunnable.run();
+        assertThat(activity.isFinishing()).isTrue();
+        assertThat(shadowOf(activity).getResultCode()).isEqualTo(Activity.RESULT_OK);
+    }
+
+    @Test
+    public void onActivityResult_ok_tvInputInfo_null() {
+        TestSetupUtils setupUtils = new TestSetupUtils(RuntimeEnvironment.application);
+        testSingletonApp.setupUtils = setupUtils;
+        FakeTvInputManager tvInputManager =
+                testSingletonApp.tvInputManagerHelper.getFakeTvInputManager();
+        SetupPassthroughActivity activity = createSetupActivityFor(testInput.getId());
+        activity.onActivityResult(REQUEST_START_SETUP_ACTIVITY, Activity.RESULT_OK, null);
+
+        assertThat(tvInputManager.getTvInputInfo(testInput.getId())).isEqualTo(null);
+        assertThat(testSingletonApp.epgFetcher.fetchStarted).isFalse();
+        assertThat(activity.isFinishing()).isTrue();
+
+        assertThat(shadowOf(activity).getResultCode()).isEqualTo(Activity.RESULT_OK);
+    }
+
+    private SetupPassthroughActivity createSetupActivityFor(String inputId) {
+        return Robolectric.buildActivity(
+                        SetupPassthroughActivity.class,
+                        CommonUtils.createSetupIntent(new Intent(), inputId))
+                .create()
+                .get();
+    }
+
+    private TvInputInfo createMockInput(String inputId) {
+        TvInputInfo tvInputInfo = Mockito.mock(TvInputInfo.class);
+        ServiceInfo serviceInfo = new ServiceInfo();
+        ApplicationInfo applicationInfo = new ApplicationInfo();
+        Mockito.when(tvInputInfo.getId()).thenReturn(inputId);
+        serviceInfo.packageName = inputId.substring(0, inputId.indexOf('/'));
+        serviceInfo.applicationInfo = applicationInfo;
+        applicationInfo.flags = 0;
+        Mockito.when(tvInputInfo.getServiceInfo()).thenReturn(serviceInfo);
+        Mockito.when(tvInputInfo.loadLabel(ArgumentMatchers.any())).thenReturn("testLabel");
+        if (VERSION.SDK_INT >= VERSION_CODES.N) {
+            Mockito.when(tvInputInfo.loadCustomLabel(ArgumentMatchers.any()))
+                    .thenReturn("testCustomLabel");
+        }
+        return tvInputInfo;
+    }
+
+    /**
+     * Test SetupUtils.
+     *
+     * <p>SetupUtils has lots of DB and threading interactions, that make it hard to test. This
+     * bypasses all of that.
+     */
+    private static class TestSetupUtils extends SetupUtils {
+        public String finishedId;
+        public Runnable finishedRunnable;
+
+        private TestSetupUtils(Context context) {
+            super(context, Optional.absent());
+        }
+
+        @Override
+        public void onTvInputSetupFinished(String inputId, @Nullable Runnable postRunnable) {
+            finishedId = inputId;
+            finishedRunnable = postRunnable;
+        }
+    }
+
+    /** Test app for {@link SetupPassthroughActivityTest} */
+    public static class MyTestApp extends TestSingletonApp implements HasAndroidInjector {
+
+        @Inject DispatchingAndroidInjector<Object> dispatchingAndroidInjector;
+
+        @Override
+        public void onCreate() {
+
+            super.onCreate();
+            // Inject afterwards so we can use objects created in super.
+            // Note TestSingletonApp does not do injection so it is safe
+            applicationInjector().inject(this);
+        }
+
+        @Override
+        public AndroidInjector<Object> androidInjector() {
+            return dispatchingAndroidInjector;
+        }
+
+        protected AndroidInjector<MyTestApp> applicationInjector() {
+
+            return DaggerSetupPassthroughActivityTest_TestComponent.builder()
+                    .applicationModule(new ApplicationModule(this))
+                    .tvSingletonsModule(new TvSingletonsModule(this))
+                    .testModule(new TestModule(this))
+                    .settableFlagsModule(flagsModule)
+                    .build();
+        }
+    }
+
+    /** Dagger component for {@link SetupPassthroughActivityTest}. */
+    @Singleton
+    @Component(
+            modules = {
+                AndroidInjectionModule.class,
+                TestModule.class,
+            })
+    interface TestComponent extends AndroidInjector<MyTestApp> {}
+
+    @Module(
+            includes = {
+                SetupPassthroughActivity.Module.class,
+                ApplicationModule.class,
+                TvSingletonsModule.class,
+                SettableFlagsModule.class,
+            })
+    /** Module for {@link MyTestApp} */
+    static class TestModule {
+        private final MyTestApp myTestApp;
+
+        TestModule(MyTestApp test) {
+            myTestApp = test;
+        }
+
+        @Provides
+        Optional<BuiltInTunerManager> providesBuiltInTunerManager() {
+            return Optional.absent();
+        }
+
+        @Provides
+        TvInputManagerHelper providesTvInputManagerHelper() {
+            return myTestApp.tvInputManagerHelper;
+        }
+
+        @Provides
+        SetupUtils providesTestSetupUtils() {
+            return myTestApp.setupUtils;
+        }
+
+        @Provides
+        @DbExecutor
+        Executor providesDbExecutor() {
+            return myTestApp.dbExecutor;
+        }
+
+        @Provides
+        ChannelDataManager providesChannelDataManager() {
+            return myTestApp.getChannelDataManager();
+        }
+
+        @Provides
+        EpgFetcher providesEpgFetcher() {
+            return myTestApp.epgFetcher;
+        }
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/ShadowTvView.java b/tests/robotests/src/com/android/tv/ShadowTvView.java
new file mode 100644
index 0000000..8aad9f0
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/ShadowTvView.java
@@ -0,0 +1,106 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.media.tv.TvTrackInfo;
+import android.media.tv.TvView;
+import android.util.AttributeSet;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowView;
+
+// TODO(b/78304522): move this class to robolectric
+/** Shadow class of {@link TvView}. */
+@Implements(TvView.class)
+public class ShadowTvView extends ShadowView {
+    public Map<Integer, String> mSelectedTracks = new HashMap<>();
+    public Map<Integer, List<TvTrackInfo>> mTracks = new HashMap<>();
+    public MainActivity.MyOnTuneListener listener;
+    public TvView.TvInputCallback mCallback;
+    public boolean mAudioTrackCountChanged;
+
+    @Implementation
+    public void __constructor__(Context context) {
+    }
+
+    @Implementation
+    public void __constructor__(Context context, AttributeSet attrs) {
+    }
+
+    @Override
+    public void __constructor__(Context context, AttributeSet attrs, int defStyleAttr) {
+    }
+
+    @Implementation
+    public List<TvTrackInfo> getTracks(int type) {
+        return mTracks.get(type);
+    }
+
+    @Implementation
+    public void selectTrack(int type, String trackId) {
+        mSelectedTracks.put(type, trackId);
+        int infoIndex = findTrackIndex(type, trackId);
+        // for some drivers, audio track count is set to 0 until the corresponding track is
+        // selected. Here we replace the track with another one whose audio track count is non-zero
+        // to test this case.
+        if (mAudioTrackCountChanged) {
+            replaceTrack(type, infoIndex);
+        }
+        mCallback.onTrackSelected("fakeInputId", type, trackId);
+    }
+
+    @Implementation
+    public String getSelectedTrack(int type) {
+        return mSelectedTracks.get(type);
+    }
+
+    @Implementation
+    public void setCallback(TvView.TvInputCallback callback) {
+        mCallback = callback;
+    }
+
+    private int findTrackIndex(int type, String trackId) {
+        List<TvTrackInfo> tracks = mTracks.get(type);
+        if (tracks == null) {
+            return -1;
+        }
+        for (int i = 0; i < tracks.size(); i++) {
+            TvTrackInfo info = tracks.get(i);
+            if (info.getId().equals(trackId)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    private void replaceTrack(int type, int trackIndex) {
+        if (trackIndex >= 0) {
+            TvTrackInfo info = mTracks.get(type).get(trackIndex);
+            info = new TvTrackInfo
+                    .Builder(info.getType(), info.getId())
+                    .setLanguage(info.getLanguage())
+                    .setAudioChannelCount(info.getAudioChannelCount() + 2)
+                    .build();
+            mTracks.get(type).set(trackIndex, info);
+        }
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/TvActivityTest.java b/tests/robotests/src/com/android/tv/TvActivityTest.java
new file mode 100644
index 0000000..81160dd
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/TvActivityTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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;
+
+import android.content.Intent;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.android.tv.util.Utils;
+import com.google.android.libraries.testing.truth.IntentSubject;
+import com.google.common.truth.Truth;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowApplication;
+
+/** Test for {@link TvActivity}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class TvActivityTest {
+
+    @Test
+    public void testLifeCycle() {
+        TvActivity activity = Robolectric.setupActivity(TvActivity.class);
+        Truth.assertThat(activity.isFinishing()).isTrue();
+
+        Intent nextStartedActivity = ShadowApplication.getInstance().getNextStartedActivity();
+        IntentSubject.assertThat(nextStartedActivity).hasComponentClass(MainActivity.class);
+        IntentSubject.assertThat(nextStartedActivity).hasExtra(Utils.EXTRA_KEY_FROM_LAUNCHER, true);
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/audio/AudioManagerHelperTest.java b/tests/robotests/src/com/android/tv/audio/AudioManagerHelperTest.java
new file mode 100644
index 0000000..e96e7df
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/audio/AudioManagerHelperTest.java
@@ -0,0 +1,260 @@
+/*
+ * 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.audio;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Activity;
+import android.media.AudioManager;
+import android.os.Build;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.android.tv.ui.api.TunableTvViewPlayingApi;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link AudioManagerHelper}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class AudioManagerHelperTest {
+
+    private AudioManagerHelper mAudioManagerHelper;
+    private TestTvView mTvView;
+    private AudioManager mAudioManager;
+
+    @Before
+    public void setup() {
+        Activity testActivity = Robolectric.buildActivity(Activity.class).get();
+        mTvView = new TestTvView();
+        mAudioManager = RuntimeEnvironment.application.getSystemService(AudioManager.class);
+
+        mAudioManagerHelper = new AudioManagerHelper(testActivity, mTvView);
+    }
+
+    @Test
+    public void onAudioFocusChange_none_noTimeShift() {
+        mTvView.mTimeShiftAvailable = false;
+
+        mAudioManagerHelper.onAudioFocusChange(AudioManager.AUDIOFOCUS_NONE);
+
+        assertThat(mTvView.mPaused).isNull();
+        assertThat(mTvView.mVolume).isZero();
+    }
+
+    @Test
+    public void onAudioFocusChange_none_TimeShift() {
+        mTvView.mTimeShiftAvailable = true;
+
+        mAudioManagerHelper.onAudioFocusChange(AudioManager.AUDIOFOCUS_NONE);
+
+        assertThat(mTvView.mPaused).isTrue();
+        assertThat(mTvView.mVolume).isNull();
+    }
+
+    @Test
+    public void onAudioFocusChange_gain_noTimeShift() {
+        mTvView.mTimeShiftAvailable = false;
+
+        mAudioManagerHelper.onAudioFocusChange(AudioManager.AUDIOFOCUS_GAIN);
+
+        assertThat(mTvView.mPaused).isNull();
+        assertThat(mTvView.mVolume).isEqualTo(1.0f);
+    }
+
+    @Test
+    public void onAudioFocusChange_gain_timeShift() {
+        mTvView.mTimeShiftAvailable = true;
+
+        mAudioManagerHelper.onAudioFocusChange(AudioManager.AUDIOFOCUS_GAIN);
+
+        assertThat(mTvView.mPaused).isFalse();
+        assertThat(mTvView.mVolume).isNull();
+    }
+
+    @Test
+    public void onAudioFocusChange_loss_noTimeShift() {
+        mTvView.mTimeShiftAvailable = false;
+
+        mAudioManagerHelper.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS);
+
+        assertThat(mTvView.mPaused).isNull();
+        assertThat(mTvView.mVolume).isEqualTo(0.0f);
+    }
+
+    @Test
+    public void onAudioFocusChange_loss_timeShift() {
+        mTvView.mTimeShiftAvailable = true;
+
+        mAudioManagerHelper.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS);
+
+        assertThat(mTvView.mPaused).isTrue();
+        assertThat(mTvView.mVolume).isNull();
+    }
+
+    @Test
+    public void onAudioFocusChange_lossTransient_noTimeShift() {
+        mTvView.mTimeShiftAvailable = false;
+
+        mAudioManagerHelper.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
+
+        assertThat(mTvView.mPaused).isNull();
+        assertThat(mTvView.mVolume).isEqualTo(0.0f);
+    }
+
+    @Test
+    public void onAudioFocusChange_lossTransient_timeShift() {
+        mTvView.mTimeShiftAvailable = true;
+
+        mAudioManagerHelper.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
+
+        assertThat(mTvView.mPaused).isTrue();
+        assertThat(mTvView.mVolume).isNull();
+    }
+
+    @Test
+    public void onAudioFocusChange_lossTransientCanDuck_noTimeShift() {
+        mTvView.mTimeShiftAvailable = false;
+
+        mAudioManagerHelper.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK);
+
+        assertThat(mTvView.mPaused).isNull();
+        assertThat(mTvView.mVolume).isEqualTo(0.3f);
+    }
+
+    @Test
+    public void onAudioFocusChange_lossTransientCanDuck_timeShift() {
+        mTvView.mTimeShiftAvailable = true;
+
+        mAudioManagerHelper.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK);
+
+        assertThat(mTvView.mPaused).isTrue();
+        assertThat(mTvView.mVolume).isNull();
+    }
+
+    @Test
+    @Config(sdk = {ConfigConstants.SDK, Build.VERSION_CODES.O})
+    public void requestAudioFocus_granted() {
+        Shadows.shadowOf(mAudioManager)
+                .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+        mAudioManagerHelper.requestAudioFocus();
+
+        assertThat(mTvView.mPaused).isNull();
+        assertThat(mTvView.mVolume).isEqualTo(1.0f);
+    }
+
+    @Test
+    @Config(sdk = {ConfigConstants.SDK, Build.VERSION_CODES.O})
+    public void requestAudioFocus_failed() {
+        Shadows.shadowOf(mAudioManager)
+                .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_FAILED);
+
+        mAudioManagerHelper.requestAudioFocus();
+
+        assertThat(mTvView.mPaused).isNull();
+        assertThat(mTvView.mVolume).isZero();
+    }
+
+    @Test
+    @Config(sdk = {ConfigConstants.SDK, Build.VERSION_CODES.O})
+    public void requestAudioFocus_delayed() {
+        Shadows.shadowOf(mAudioManager)
+                .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_DELAYED);
+
+        mAudioManagerHelper.requestAudioFocus();
+
+        assertThat(mTvView.mPaused).isNull();
+        assertThat(mTvView.mVolume).isZero();
+    }
+
+    @Test
+    public void setVolumeByAudioFocusStatus_started() {
+        mAudioManagerHelper.setVolumeByAudioFocusStatus();
+
+        assertThat(mTvView.mPaused).isNull();
+        assertThat(mTvView.mVolume).isZero();
+    }
+
+    @Test
+    public void setVolumeByAudioFocusStatus_notStarted() {
+        mTvView.mStarted = false;
+        mAudioManagerHelper.setVolumeByAudioFocusStatus();
+
+        assertThat(mTvView.mPaused).isNull();
+        assertThat(mTvView.mVolume).isNull();
+    }
+
+    private static class TestTvView implements TunableTvViewPlayingApi {
+        private boolean mStarted = true;
+        private boolean mTimeShiftAvailable = false;
+        private Float mVolume = null;
+        private Boolean mPaused = null;
+
+        @Override
+        public boolean isPlaying() {
+            return mStarted;
+        }
+
+        @Override
+        public void setStreamVolume(float volume) {
+            mVolume = volume;
+        }
+
+        @Override
+        public void setTimeShiftListener(TimeShiftListener listener) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean isTimeShiftAvailable() {
+            return mTimeShiftAvailable;
+        }
+
+        @Override
+        public void timeShiftPlay() {
+            mPaused = false;
+        }
+
+        @Override
+        public void timeShiftPause() {
+            mPaused = true;
+        }
+
+        @Override
+        public void timeShiftRewind(int speed) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void timeShiftFastForward(int speed) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void timeShiftSeekTo(long timeMs) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public long timeShiftGetCurrentPositionMs() {
+            throw new UnsupportedOperationException();
+        }
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/data/ChannelNumberTest.java b/tests/robotests/src/com/android/tv/data/ChannelNumberTest.java
new file mode 100644
index 0000000..14c9bc4
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/data/ChannelNumberTest.java
@@ -0,0 +1,174 @@
+/*
+ * 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.data;
+
+import static com.android.tv.data.ChannelNumber.parseChannelNumber;
+import static com.android.tv.testing.ChannelNumberSubject.assertThat;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.tv.testing.ComparableTester;
+import com.android.tv.testing.constants.ConfigConstants;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link ChannelNumber}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class ChannelNumberTest {
+
+    @Test
+    public void newChannelNumber() {
+        assertThat(new ChannelNumber()).isEmpty();
+    }
+
+    @Test
+    public void parseChannelNumber_empty() {
+        assertThat(parseChannelNumber("")).isNull();
+    }
+
+    @Test
+    public void parseChannelNumber_dash() {
+        assertThat(parseChannelNumber("-")).isNull();
+    }
+
+    @Test
+    public void parseChannelNumber_abcd12() {
+        assertThat(parseChannelNumber("abcd12")).isNull();
+    }
+
+    @Test
+    public void parseChannelNumber_12abcd() {
+        assertThat(parseChannelNumber("12abcd")).isNull();
+    }
+
+    @Test
+    public void parseChannelNumber_dash12() {
+        assertThat(parseChannelNumber("-12")).isNull();
+    }
+
+    @Test
+    public void parseChannelNumber_1() {
+        assertThat(parseChannelNumber("1")).displaysAs(1);
+    }
+
+    @Test
+    public void parseChannelNumber_1234dash4321() {
+        assertThat(parseChannelNumber("1234-4321")).displaysAs(1234, 4321);
+    }
+
+    @Test
+    public void parseChannelNumber_3dash4() {
+        assertThat(parseChannelNumber("3-4")).displaysAs(3, 4);
+    }
+
+    @Test
+    public void parseChannelNumber_5dash6() {
+        assertThat(parseChannelNumber("5-6")).displaysAs(5, 6);
+    }
+
+    @Test
+    public void compareTo() {
+        new ComparableTester<ChannelNumber>()
+                .addEquivalentGroup(parseChannelNumber("1"), parseChannelNumber("1"))
+                .addEquivalentGroup(parseChannelNumber("2"))
+                .addEquivalentGroup(parseChannelNumber("2-1"))
+                .addEquivalentGroup(parseChannelNumber("2-2"))
+                .addEquivalentGroup(parseChannelNumber("2-10"))
+                .addEquivalentGroup(parseChannelNumber("3"))
+                .addEquivalentGroup(parseChannelNumber("4"), parseChannelNumber("4-0"))
+                .addEquivalentGroup(parseChannelNumber("10"))
+                .addEquivalentGroup(parseChannelNumber("100"))
+                .test();
+    }
+
+    @Test
+    public void compare_null_null() {
+        assertThat(ChannelNumber.compare(null, null)).isEqualTo(0);
+    }
+
+    @Test
+    public void compare_1_1() {
+        assertThat(ChannelNumber.compare("1", "1")).isEqualTo(0);
+        ;
+    }
+
+    @Test
+    public void compare_null_1() {
+        assertThat(ChannelNumber.compare(null, "1")).isLessThan(0);
+    }
+
+    @Test
+    public void compare_abcd_1() {
+        assertThat(ChannelNumber.compare("abcd", "1")).isLessThan(0);
+    }
+
+    @Test
+    public void compare_dash1_1() {
+        assertThat(ChannelNumber.compare(".4", "1")).isLessThan(0);
+    }
+
+    @Test
+    public void compare_1_null() {
+        assertThat(ChannelNumber.compare("1", null)).isGreaterThan(0);
+    }
+
+    @Test
+    public void equivalent_null_to_null() {
+        assertThat(ChannelNumber.equivalent(null, null)).isTrue();
+    }
+
+    @Test
+    public void equivalent_1_to_1() {
+        assertThat(ChannelNumber.equivalent("1", "1")).isTrue();
+    }
+
+    @Test
+    public void equivalent_1d2_to_1() {
+        assertThat(ChannelNumber.equivalent("1-2", "1")).isTrue();
+    }
+
+    @Test
+    public void equivalent_1_to_1d2() {
+        assertThat(ChannelNumber.equivalent("1", "1-2")).isTrue();
+    }
+
+    @Test
+    public void equivalent_1_to_2_isFalse() {
+        assertThat(ChannelNumber.equivalent("1", "2")).isFalse();
+    }
+
+    @Test
+    public void equivalent_1d1_to_1d1() {
+        assertThat(ChannelNumber.equivalent("1-1", "1-1")).isTrue();
+    }
+
+    @Test
+    public void equivalent_1d1_to_1d2_isFalse() {
+        assertThat(ChannelNumber.equivalent("1-1", "1-2")).isFalse();
+    }
+
+    @Test
+    public void equivalent_1_to_null_isFalse() {
+        assertThat(ChannelNumber.equivalent("1", null)).isFalse();
+    }
+
+    @Test
+    public void equivalent_null_to_1_isFalse() {
+        assertThat(ChannelNumber.equivalent(null, "1")).isFalse();
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/data/GenreItemTest.java b/tests/robotests/src/com/android/tv/data/GenreItemTest.java
new file mode 100644
index 0000000..c7adce2
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/data/GenreItemTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.data;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.media.tv.TvContract.Programs.Genres;
+import android.os.Build;
+import com.android.tv.testing.constants.ConfigConstants;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link GenreItems}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class GenreItemTest {
+    private static final String INVALID_GENRE = "INVALID GENRE";
+
+    @Test
+    public void testGetLabels() {
+        // Checks if no exception is thrown.
+        GenreItems.getLabels(RuntimeEnvironment.application);
+    }
+
+    @Test
+    public void testGetCanonicalGenre() {
+        int count = GenreItems.getGenreCount();
+        assertThat(GenreItems.getCanonicalGenre(GenreItems.ID_ALL_CHANNELS)).isNull();
+        for (int i = 1; i < count; ++i) {
+            assertThat(GenreItems.getCanonicalGenre(i)).isNotNull();
+        }
+    }
+
+    @Test
+    public void testGetId_base() {
+        int count = GenreItems.getGenreCount();
+        assertThat(GenreItems.getId(null)).isEqualTo(GenreItems.ID_ALL_CHANNELS);
+        assertThat(GenreItems.getId(INVALID_GENRE)).isEqualTo(GenreItems.ID_ALL_CHANNELS);
+        assertInRange(GenreItems.getId(Genres.FAMILY_KIDS), 1, count - 1);
+        assertInRange(GenreItems.getId(Genres.SPORTS), 1, count - 1);
+        assertInRange(GenreItems.getId(Genres.SHOPPING), 1, count - 1);
+        assertInRange(GenreItems.getId(Genres.MOVIES), 1, count - 1);
+        assertInRange(GenreItems.getId(Genres.COMEDY), 1, count - 1);
+        assertInRange(GenreItems.getId(Genres.TRAVEL), 1, count - 1);
+        assertInRange(GenreItems.getId(Genres.DRAMA), 1, count - 1);
+        assertInRange(GenreItems.getId(Genres.EDUCATION), 1, count - 1);
+        assertInRange(GenreItems.getId(Genres.ANIMAL_WILDLIFE), 1, count - 1);
+        assertInRange(GenreItems.getId(Genres.NEWS), 1, count - 1);
+        assertInRange(GenreItems.getId(Genres.GAMING), 1, count - 1);
+    }
+
+    @Test
+    public void testGetId_lmp_mr1() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
+            assertThat(GenreItems.getId(Genres.ARTS)).isEqualTo(GenreItems.ID_ALL_CHANNELS);
+            assertThat(GenreItems.getId(Genres.ENTERTAINMENT))
+                    .isEqualTo(GenreItems.ID_ALL_CHANNELS);
+            assertThat(GenreItems.getId(Genres.LIFE_STYLE)).isEqualTo(GenreItems.ID_ALL_CHANNELS);
+            assertThat(GenreItems.getId(Genres.MUSIC)).isEqualTo(GenreItems.ID_ALL_CHANNELS);
+            assertThat(GenreItems.getId(Genres.PREMIER)).isEqualTo(GenreItems.ID_ALL_CHANNELS);
+            assertThat(GenreItems.getId(Genres.TECH_SCIENCE)).isEqualTo(GenreItems.ID_ALL_CHANNELS);
+        } else {
+            int count = GenreItems.getGenreCount();
+            assertInRange(GenreItems.getId(Genres.ARTS), 1, count - 1);
+            assertInRange(GenreItems.getId(Genres.ENTERTAINMENT), 1, count - 1);
+            assertInRange(GenreItems.getId(Genres.LIFE_STYLE), 1, count - 1);
+            assertInRange(GenreItems.getId(Genres.MUSIC), 1, count - 1);
+            assertInRange(GenreItems.getId(Genres.PREMIER), 1, count - 1);
+            assertInRange(GenreItems.getId(Genres.TECH_SCIENCE), 1, count - 1);
+        }
+    }
+
+    private void assertInRange(int value, int lower, int upper) {
+        assertThat(value >= lower && value <= upper).isTrue();
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/data/PreviewDataManagerTest.java b/tests/robotests/src/com/android/tv/data/PreviewDataManagerTest.java
new file mode 100644
index 0000000..cfbf315
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/data/PreviewDataManagerTest.java
@@ -0,0 +1,214 @@
+/*
+ * 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.data;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.pm.ProviderInfo;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Log;
+import android.util.Pair;
+
+import androidx.tvprovider.media.tv.PreviewProgram;
+import androidx.tvprovider.media.tv.TvContractCompat;
+
+import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.testing.constants.ConfigConstants;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowContentResolver;
+import org.robolectric.shadows.ShadowLog;
+
+import java.util.List;
+
+/** Tests for {@link PreviewDataManager}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class PreviewDataManagerTest {
+    private static final long FAKE_PREVIEW_CHANNEL_ID = 2002;
+    private static final long FAKE_PROGRAM_ID = 1001;
+    private static final String FAKE_PROGRAM_TITLE = "test program";
+    private static final String FAKE_PROGRAM_POSTER_URI = "http://fake.uri/poster.jpg";
+    private static final long FAKE_CHANNEL_ID = 1002;
+    private static final String FAKE_CHANNEL_DISPLAY_NAME = "test channel";
+    private static final String FAKE_INPUT_ID = "test input";
+
+    private static class QueryExceptionProvider extends ContentProvider {
+        @Override
+        public boolean onCreate() {
+            return false;
+        }
+
+        @Nullable
+        @Override
+        public Cursor query(
+                @NonNull Uri uri,
+                @Nullable String[] projection,
+                @Nullable String selection,
+                @Nullable String[] selectionArgs,
+                @Nullable String sortOrder) {
+            throw new SQLException("Testing " + uri);
+        }
+
+        @Nullable
+        @Override
+        public String getType(@NonNull Uri uri) {
+            return null;
+        }
+
+        @Nullable
+        @Override
+        public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
+            return null;
+        }
+
+        @Override
+        public int delete(
+                @NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
+            return 0;
+        }
+
+        @Override
+        public int update(
+                @NonNull Uri uri,
+                @Nullable ContentValues values,
+                @Nullable String selection,
+                @Nullable String[] selectionArgs) {
+            return 0;
+        }
+    }
+
+    @Test
+    public void start() {
+        PreviewDataManager previewDataManager =
+                new PreviewDataManager(RuntimeEnvironment.application);
+        assertThat(previewDataManager.isLoadFinished()).isFalse();
+        previewDataManager.start();
+        assertThat(previewDataManager.isLoadFinished()).isTrue();
+    }
+
+    @Test
+    public void queryPreviewData_sqlexception() {
+        ProviderInfo info = new ProviderInfo();
+        info.authority = TvContractCompat.AUTHORITY;
+        QueryExceptionProvider provider =
+                Robolectric.buildContentProvider(QueryExceptionProvider.class).create(info).get();
+        ShadowContentResolver.registerProviderInternal(TvContractCompat.AUTHORITY, provider);
+
+        PreviewDataManager previewDataManager =
+                new PreviewDataManager(RuntimeEnvironment.application);
+        assertThat(previewDataManager.isLoadFinished()).isFalse();
+        previewDataManager.start();
+        List<ShadowLog.LogItem> logs = ShadowLog.getLogsForTag("PreviewDataManager");
+        // The only warning should be the test one
+        // NOTE: I am not using hamcrest matchers because of weird class loading issues
+        // TODO: use truth
+        for (ShadowLog.LogItem log : logs) {
+            if (log.type == Log.WARN) {
+                assertThat(log.msg).isEqualTo("Unable to get preview data");
+                assertThat(log.throwable).isInstanceOf(SQLException.class);
+                assertThat(log.throwable)
+                        .hasMessageThat()
+                        .isEqualTo("Testing content://android.media.tv/channel?preview=true");
+            }
+        }
+    }
+
+    @Test
+    public void createPreviewProgram_fromProgram() {
+        Program program =
+                new ProgramImpl.Builder()
+                        .setId(FAKE_PROGRAM_ID)
+                        .setTitle(FAKE_PROGRAM_TITLE)
+                        .setPosterArtUri(FAKE_PROGRAM_POSTER_URI)
+                        .build();
+        Channel channel =
+                new ChannelImpl.Builder()
+                        .setId(FAKE_CHANNEL_ID)
+                        .setDisplayName(FAKE_CHANNEL_DISPLAY_NAME)
+                        .setInputId(FAKE_INPUT_ID)
+                        .build();
+
+        PreviewProgram previewProgram =
+                PreviewDataManager.PreviewDataUtils.createPreviewProgramFromContent(
+                        PreviewProgramContent.createFromProgram(
+                                FAKE_PREVIEW_CHANNEL_ID, program, channel),
+                        0);
+
+        assertThat(previewProgram.getChannelId()).isEqualTo(FAKE_PREVIEW_CHANNEL_ID);
+        assertThat(previewProgram.getType())
+                .isEqualTo(TvContractCompat.PreviewPrograms.TYPE_CHANNEL);
+        assertThat(previewProgram.isLive()).isTrue();
+        assertThat(previewProgram.getTitle()).isEqualTo(FAKE_PROGRAM_TITLE);
+        assertThat(previewProgram.getDescription()).isEqualTo(FAKE_CHANNEL_DISPLAY_NAME);
+        assertThat(previewProgram.getPosterArtUri().toString()).isEqualTo(FAKE_PROGRAM_POSTER_URI);
+        assertThat(previewProgram.getIntentUri()).isEqualTo(channel.getUri());
+        assertThat(previewProgram.getPreviewVideoUri())
+                .isEqualTo(
+                        PreviewDataManager.PreviewDataUtils.addQueryParamToUri(
+                                channel.getUri(),
+                                Pair.create(PreviewProgramContent.PARAM_INPUT, FAKE_INPUT_ID)));
+        assertThat(previewProgram.getInternalProviderId())
+                .isEqualTo(Long.toString(FAKE_PROGRAM_ID));
+        assertThat(previewProgram.getContentId()).isEqualTo(channel.getUri().toString());
+    }
+
+    @Test
+    public void createPreviewProgram_fromRecordedProgram() {
+        RecordedProgram program =
+                RecordedProgram.builder()
+                        .setId(FAKE_PROGRAM_ID)
+                        .setTitle(FAKE_PROGRAM_TITLE)
+                        .setPosterArtUri(FAKE_PROGRAM_POSTER_URI)
+                        .setInputId(FAKE_INPUT_ID)
+                        .build();
+        Uri recordedProgramUri = TvContractCompat.buildRecordedProgramUri(FAKE_PROGRAM_ID);
+
+        PreviewProgram previewProgram =
+                PreviewDataManager.PreviewDataUtils.createPreviewProgramFromContent(
+                        PreviewProgramContent.createFromRecordedProgram(
+                                FAKE_PREVIEW_CHANNEL_ID, program, null),
+                        0);
+
+        assertThat(previewProgram.getChannelId()).isEqualTo(FAKE_PREVIEW_CHANNEL_ID);
+        assertThat(previewProgram.getType()).isEqualTo(TvContractCompat.PreviewPrograms.TYPE_CLIP);
+        assertThat(previewProgram.isLive()).isFalse();
+        assertThat(previewProgram.getTitle()).isEqualTo(FAKE_PROGRAM_TITLE);
+        assertThat(previewProgram.getDescription()).isEmpty();
+        assertThat(previewProgram.getPosterArtUri().toString()).isEqualTo(FAKE_PROGRAM_POSTER_URI);
+        assertThat(previewProgram.getIntentUri()).isEqualTo(recordedProgramUri);
+        assertThat(previewProgram.getPreviewVideoUri())
+                .isEqualTo(
+                        PreviewDataManager.PreviewDataUtils.addQueryParamToUri(
+                                recordedProgramUri,
+                                Pair.create(PreviewProgramContent.PARAM_INPUT, FAKE_INPUT_ID)));
+        assertThat(previewProgram.getContentId()).isEqualTo(recordedProgramUri.toString());
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/data/ProgramDataManagerTest.java b/tests/robotests/src/com/android/tv/data/ProgramDataManagerTest.java
new file mode 100644
index 0000000..2176aa9
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/data/ProgramDataManagerTest.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.data;
+
+import static android.os.Looper.getMainLooper;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.robolectric.Shadows.shadowOf;
+
+import android.content.ContentResolver;
+import android.media.tv.TvContract;
+
+import com.android.tv.common.flags.impl.DefaultBackendKnobsFlags;
+import com.android.tv.data.api.Program;
+import com.android.tv.perf.stub.StubPerformanceMonitor;
+import com.android.tv.testing.FakeTvInputManagerHelper;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.android.tv.testing.constants.Constants;
+import com.android.tv.testing.data.ProgramInfo;
+import com.android.tv.testing.data.ProgramUtils;
+import com.android.tv.testing.fakes.FakeClock;
+import com.android.tv.testing.fakes.FakeTvProvider;
+import com.android.tv.testing.robo.ContentProviders;
+import com.android.tv.testing.testdata.TestData;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.android.util.concurrent.RoboExecutorService;
+import org.robolectric.annotation.Config;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/** Test for {@link ProgramDataManager} */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK, application = TestSingletonApp.class)
+public class ProgramDataManagerTest {
+
+    // Wait time for expected success.
+    private static final long WAIT_TIME_OUT_MS = 1000L;
+    // Wait time for expected failure.
+    private static final long FAILURE_TIME_OUT_MS = 300L;
+
+    private ProgramDataManager mProgramDataManager;
+    private FakeClock mClock;
+    private TestProgramDataManagerCallback mCallback;
+
+    @Before
+    public void setUp() {
+        mClock = FakeClock.createWithCurrentTime();
+        mCallback = new TestProgramDataManagerCallback();
+        ContentProviders.register(FakeTvProvider.class, TvContract.AUTHORITY);
+        TestData.DEFAULT_10_CHANNELS.init(
+                RuntimeEnvironment.application, mClock, TimeUnit.DAYS.toMillis(1));
+        FakeTvInputManagerHelper tvInputManagerHelper =
+                new FakeTvInputManagerHelper(RuntimeEnvironment.application);
+        RoboExecutorService executor = new RoboExecutorService();
+        ContentResolver contentResolver = RuntimeEnvironment.application.getContentResolver();
+        ChannelDataManager channelDataManager =
+                new ChannelDataManager(
+                        RuntimeEnvironment.application,
+                        tvInputManagerHelper,
+                        executor,
+                        contentResolver);
+        mProgramDataManager =
+                new ProgramDataManager(
+                        RuntimeEnvironment.application,
+                        executor,
+                        RuntimeEnvironment.application.getContentResolver(),
+                        mClock,
+                        getMainLooper(),
+                        new DefaultBackendKnobsFlags(),
+                        new StubPerformanceMonitor(),
+                        channelDataManager,
+                        tvInputManagerHelper);
+
+        mProgramDataManager.setPrefetchEnabled(true);
+        mProgramDataManager.addCallback(mCallback);
+    }
+
+    @After
+    public void tearDown() {
+        mProgramDataManager.stop();
+    }
+
+    private void startAndWaitForComplete() throws InterruptedException {
+        mProgramDataManager.start();
+        shadowOf(getMainLooper()).idle();
+        assertThat(mCallback.channelUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS))
+                .isTrue();
+    }
+
+    /** Test for {@link ProgramInfo#getIndex} and {@link ProgramInfo#getStartTimeMs}. */
+    @Test
+    public void testProgramUtils() {
+        ProgramInfo stub = ProgramInfo.create();
+        for (long channelId = 1; channelId < Constants.UNIT_TEST_CHANNEL_COUNT; channelId++) {
+            int index = stub.getIndex(mClock.currentTimeMillis(), channelId);
+            long startTimeMs = stub.getStartTimeMs(index, channelId);
+            ProgramInfo programAt = stub.build(RuntimeEnvironment.application, index);
+            assertThat(startTimeMs).isAtMost(mClock.currentTimeMillis());
+            assertThat(mClock.currentTimeMillis()).isLessThan(startTimeMs + programAt.durationMs);
+        }
+    }
+
+    /**
+     * Test for following methods.
+     *
+     * <p>{@link ProgramDataManager#getCurrentProgram(long)}, {@link
+     * ProgramDataManager#getPrograms(long, long)}, {@link
+     * ProgramDataManager#setPrefetchTimeRange(long)}.
+     */
+    @Test
+    public void testGetPrograms() throws InterruptedException {
+        // Initial setup to test {@link ProgramDataManager#setPrefetchTimeRange(long)}.
+        long preventSnapDelayMs = ProgramDataManager.PROGRAM_GUIDE_SNAP_TIME_MS * 2;
+        long prefetchTimeRangeStartMs = System.currentTimeMillis() + preventSnapDelayMs;
+        mClock.setCurrentTimeMillis(prefetchTimeRangeStartMs + preventSnapDelayMs);
+        mProgramDataManager.setPrefetchTimeRange(prefetchTimeRangeStartMs);
+
+        startAndWaitForComplete();
+
+        for (long channelId = 1; channelId <= Constants.UNIT_TEST_CHANNEL_COUNT; channelId++) {
+            Program currentProgram = mProgramDataManager.getCurrentProgram(channelId);
+            // Test {@link ProgramDataManager#getCurrentProgram(long)}.
+            assertThat(currentProgram).isNotNull();
+            assertWithMessage("currentProgramStartTime")
+                    .that(currentProgram.getStartTimeUtcMillis())
+                    .isLessThan(mClock.currentTimeMillis());
+            assertWithMessage("currentProgramEndTime")
+                    .that(currentProgram.getEndTimeUtcMillis())
+                    .isGreaterThan(mClock.currentTimeMillis());
+
+            // Test {@link ProgramDataManager#getPrograms(long)}.
+            // Case #1: Normal case
+            List<Program> programs =
+                    mProgramDataManager.getPrograms(channelId, mClock.currentTimeMillis());
+            ProgramInfo stub = ProgramInfo.create();
+            int index = stub.getIndex(mClock.currentTimeMillis(), channelId);
+            for (Program program : programs) {
+                ProgramInfo programInfoAt = stub.build(RuntimeEnvironment.application, index);
+                long startTimeMs = stub.getStartTimeMs(index, channelId);
+                assertProgramEquals(startTimeMs, programInfoAt, program);
+                index++;
+            }
+            // Case #2: Corner cases where there's a program that starts at the start of the range.
+            long startTimeMs = programs.get(0).getStartTimeUtcMillis();
+            programs = mProgramDataManager.getPrograms(channelId, startTimeMs);
+            assertThat(programs.get(0).getStartTimeUtcMillis()).isEqualTo(startTimeMs);
+
+            // Test {@link ProgramDataManager#setPrefetchTimeRange(long)}.
+            programs =
+                    mProgramDataManager.getPrograms(
+                            channelId, prefetchTimeRangeStartMs - TimeUnit.HOURS.toMillis(1));
+            for (Program program : programs) {
+                assertThat(program.getEndTimeUtcMillis()).isAtLeast(prefetchTimeRangeStartMs);
+            }
+        }
+    }
+
+    /**
+     * Test for following methods.
+     *
+     * <p>{@link ProgramDataManager#addOnCurrentProgramUpdatedListener}, {@link
+     * ProgramDataManager#removeOnCurrentProgramUpdatedListener}.
+     */
+    @Test
+    public void testCurrentProgramListener() throws InterruptedException {
+        final long testChannelId = 1;
+        ProgramInfo stub = ProgramInfo.create();
+        int index = stub.getIndex(mClock.currentTimeMillis(), testChannelId);
+        // Set current time to few seconds before the current program ends,
+        // so we can see if callback is called as expected.
+        long nextProgramStartTimeMs = stub.getStartTimeMs(index + 1, testChannelId);
+        ProgramInfo nextProgramInfo = stub.build(RuntimeEnvironment.application, index + 1);
+        mClock.setCurrentTimeMillis(nextProgramStartTimeMs - (WAIT_TIME_OUT_MS / 2));
+
+        startAndWaitForComplete();
+        // Note that changing current time doesn't affect the current program
+        // because current program is updated after waiting for the program's duration.
+        // See {@link ProgramDataManager#updateCurrentProgram}.
+        TestProgramDataManagerOnCurrentProgramUpdatedListener listener =
+                new TestProgramDataManagerOnCurrentProgramUpdatedListener();
+        mClock.setCurrentTimeMillis(mClock.currentTimeMillis() + WAIT_TIME_OUT_MS);
+        mProgramDataManager.addOnCurrentProgramUpdatedListener(testChannelId, listener);
+        shadowOf(getMainLooper()).runToEndOfTasks();
+        assertThat(
+                        listener.currentProgramUpdatedLatch.await(
+                                WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS))
+                .isTrue();
+        assertThat(listener.updatedChannelId).isEqualTo(testChannelId);
+        Program currentProgram = mProgramDataManager.getCurrentProgram(testChannelId);
+        assertProgramEquals(nextProgramStartTimeMs, nextProgramInfo, currentProgram);
+        assertThat(currentProgram).isEqualTo(listener.updatedProgram);
+    }
+
+    /** Test if program data is refreshed after the program insertion. */
+    @Test
+    public void testContentProviderUpdate() throws InterruptedException {
+        final long testChannelId = 1;
+        startAndWaitForComplete();
+        // Force program data manager to update program data whenever it's changes.
+        mProgramDataManager.setProgramPrefetchUpdateWait(0);
+        mCallback.reset();
+        List<Program> programList =
+                mProgramDataManager.getPrograms(testChannelId, mClock.currentTimeMillis());
+        assertThat(programList).isNotNull();
+        long lastProgramEndTime = programList.get(programList.size() - 1).getEndTimeUtcMillis();
+        // Make change in content provider
+        ProgramUtils.populatePrograms(
+                RuntimeEnvironment.application,
+                TvContract.buildChannelUri(testChannelId),
+                ProgramInfo.create(),
+                mClock,
+                TimeUnit.DAYS.toMillis(2));
+        shadowOf(getMainLooper()).runToEndOfTasks();
+        assertThat(mCallback.programUpdatedLatch.await(WAIT_TIME_OUT_MS, TimeUnit.MILLISECONDS))
+                .isTrue();
+        programList = mProgramDataManager.getPrograms(testChannelId, mClock.currentTimeMillis());
+        assertThat(lastProgramEndTime)
+                .isLessThan(programList.get(programList.size() - 1).getEndTimeUtcMillis());
+    }
+
+    /** Test for {@link ProgramDataManager#setPauseProgramUpdate(boolean)}. */
+    @Test
+    public void testSetPauseProgramUpdate() throws InterruptedException {
+        final long testChannelId = 1;
+        startAndWaitForComplete();
+        // Force program data manager to update program data whenever it's changes.
+        mProgramDataManager.setProgramPrefetchUpdateWait(0);
+        mCallback.reset();
+        mProgramDataManager.setPauseProgramUpdate(true);
+        ProgramUtils.populatePrograms(
+                RuntimeEnvironment.application,
+                TvContract.buildChannelUri(testChannelId),
+                ProgramInfo.create(),
+                mClock,
+                TimeUnit.DAYS.toMillis(2));
+        shadowOf(getMainLooper()).runToEndOfTasks();
+        assertThat(mCallback.programUpdatedLatch.await(FAILURE_TIME_OUT_MS, TimeUnit.MILLISECONDS))
+                .isFalse();
+    }
+
+    public static void assertProgramEquals(
+            long expectedStartTime, ProgramInfo expectedInfo, Program actualProgram) {
+        assertWithMessage("title").that(actualProgram.getTitle()).isEqualTo(expectedInfo.title);
+        assertWithMessage("episode")
+                .that(actualProgram.getEpisodeTitle())
+                .isEqualTo(expectedInfo.episode);
+        assertWithMessage("description")
+                .that(actualProgram.getDescription())
+                .isEqualTo(expectedInfo.description);
+        assertWithMessage("startTime")
+                .that(actualProgram.getStartTimeUtcMillis())
+                .isEqualTo(expectedStartTime);
+        assertWithMessage("endTime")
+                .that(actualProgram.getEndTimeUtcMillis())
+                .isEqualTo(expectedStartTime + expectedInfo.durationMs);
+    }
+
+    private static class TestProgramDataManagerCallback implements ProgramDataManager.Callback {
+        public CountDownLatch programUpdatedLatch = new CountDownLatch(1);
+        public CountDownLatch channelUpdatedLatch = new CountDownLatch(1);
+
+        @Override
+        public void onProgramUpdated() {
+            programUpdatedLatch.countDown();
+        }
+
+        @Override
+        public void onChannelUpdated() {
+            channelUpdatedLatch.countDown();
+        }
+
+        public void reset() {
+            programUpdatedLatch = new CountDownLatch(1);
+            channelUpdatedLatch = new CountDownLatch(1);
+        }
+    }
+
+    private static class TestProgramDataManagerOnCurrentProgramUpdatedListener
+            implements OnCurrentProgramUpdatedListener {
+        public final CountDownLatch currentProgramUpdatedLatch = new CountDownLatch(1);
+        public long updatedChannelId = -1;
+        public Program updatedProgram = null;
+
+        @Override
+        public void onCurrentProgramUpdated(long channelId, Program program) {
+            updatedChannelId = channelId;
+            updatedProgram = program;
+            currentProgramUpdatedLatch.countDown();
+        }
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/data/ProgramTest.java b/tests/robotests/src/com/android/tv/data/ProgramTest.java
new file mode 100644
index 0000000..4f0c889
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/data/ProgramTest.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.data;
+
+import static android.media.tv.TvContract.Programs.Genres.COMEDY;
+import static android.media.tv.TvContract.Programs.Genres.FAMILY_KIDS;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.media.tv.TvContentRating;
+import android.media.tv.TvContract.Programs.Genres;
+import android.os.Parcel;
+
+import com.android.tv.data.api.Program;
+import com.android.tv.data.api.Program.CriticScore;
+import com.android.tv.testing.constants.ConfigConstants;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** Tests for {@link ProgramImpl}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class ProgramTest {
+    private static final int NOT_FOUND_GENRE = 987;
+
+    private static final int FAMILY_GENRE_ID = GenreItems.getId(FAMILY_KIDS);
+
+    private static final int COMEDY_GENRE_ID = GenreItems.getId(COMEDY);
+
+    @Test
+    public void testBuild() {
+        Program program = new ProgramImpl.Builder().build();
+        assertWithMessage("isValid").that(program.isValid()).isFalse();
+    }
+
+    @Test
+    public void testNoGenres() {
+        Program program = new ProgramImpl.Builder().setCanonicalGenres("").build();
+        assertNullCanonicalGenres(program);
+        assertHasGenre(program, NOT_FOUND_GENRE, false);
+        assertHasGenre(program, FAMILY_GENRE_ID, false);
+        assertHasGenre(program, COMEDY_GENRE_ID, false);
+        assertHasGenre(program, GenreItems.ID_ALL_CHANNELS, true);
+    }
+
+    @Test
+    public void testFamilyGenre() {
+        Program program = new ProgramImpl.Builder().setCanonicalGenres(FAMILY_KIDS).build();
+        assertCanonicalGenres(program, FAMILY_KIDS);
+        assertHasGenre(program, NOT_FOUND_GENRE, false);
+        assertHasGenre(program, FAMILY_GENRE_ID, true);
+        assertHasGenre(program, COMEDY_GENRE_ID, false);
+        assertHasGenre(program, GenreItems.ID_ALL_CHANNELS, true);
+    }
+
+    @Test
+    public void testFamilyComedyGenre() {
+        Program program =
+                new ProgramImpl.Builder().setCanonicalGenres(FAMILY_KIDS + ", " + COMEDY).build();
+        assertCanonicalGenres(program, FAMILY_KIDS, COMEDY);
+        assertHasGenre(program, NOT_FOUND_GENRE, false);
+        assertHasGenre(program, FAMILY_GENRE_ID, true);
+        assertHasGenre(program, COMEDY_GENRE_ID, true);
+        assertHasGenre(program, GenreItems.ID_ALL_CHANNELS, true);
+    }
+
+    @Test
+    public void testOtherGenre() {
+        Program program = new ProgramImpl.Builder().setCanonicalGenres("other").build();
+        assertCanonicalGenres(program);
+        assertHasGenre(program, NOT_FOUND_GENRE, false);
+        assertHasGenre(program, FAMILY_GENRE_ID, false);
+        assertHasGenre(program, COMEDY_GENRE_ID, false);
+        assertHasGenre(program, GenreItems.ID_ALL_CHANNELS, true);
+    }
+
+    @Test
+    public void testParcelable() {
+        List<CriticScore> criticScores = new ArrayList<>();
+        criticScores.add(new CriticScore("1", "2", "3"));
+        criticScores.add(new CriticScore("4", "5", "6"));
+        ImmutableList<TvContentRating> ratings =
+                ImmutableList.of(
+                        TvContentRating.unflattenFromString("1/2/3"),
+                        TvContentRating.unflattenFromString("4/5/6"));
+        ProgramImpl p =
+                new ProgramImpl.Builder()
+                        .setId(1)
+                        .setPackageName("2")
+                        .setChannelId(3)
+                        .setTitle("4")
+                        .setSeriesId("5")
+                        .setEpisodeTitle("6")
+                        .setSeasonNumber("7")
+                        .setSeasonTitle("8")
+                        .setEpisodeNumber("9")
+                        .setStartTimeUtcMillis(10)
+                        .setEndTimeUtcMillis(11)
+                        .setDescription("12")
+                        .setLongDescription("12-long")
+                        .setVideoWidth(13)
+                        .setVideoHeight(14)
+                        .setCriticScores(criticScores)
+                        .setPosterArtUri("15")
+                        .setThumbnailUri("16")
+                        .setCanonicalGenres(Genres.encode(Genres.SPORTS, Genres.SHOPPING))
+                        .setContentRatings(ratings)
+                        .setRecordingProhibited(true)
+                        .build();
+        Parcel p1 = Parcel.obtain();
+        Parcel p2 = Parcel.obtain();
+        try {
+            p.writeToParcel(p1, 0);
+            byte[] bytes = p1.marshall();
+            p2.unmarshall(bytes, 0, bytes.length);
+            p2.setDataPosition(0);
+            ProgramImpl r2 = ProgramImpl.fromParcel(p2);
+            assertThat(r2).isEqualTo(p);
+        } finally {
+            p1.recycle();
+            p2.recycle();
+        }
+    }
+
+    @Test
+    public void testParcelableWithCriticScore() {
+        ProgramImpl program =
+                new ProgramImpl.Builder()
+                        .setTitle("MyTitle")
+                        .addCriticScore(
+                                new CriticScore(
+                                        "default source", "5/10", "http://testurl/testimage.jpg"))
+                        .build();
+        Parcel parcel = Parcel.obtain();
+        program.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        Program programFromParcel = ProgramImpl.CREATOR.createFromParcel(parcel);
+
+        assertThat(programFromParcel.getCriticScores()).isNotNull();
+        assertThat(programFromParcel.getCriticScores().get(0).source).isEqualTo("default source");
+        assertThat(programFromParcel.getCriticScores().get(0).score).isEqualTo("5/10");
+        assertThat(programFromParcel.getCriticScores().get(0).logoUrl)
+                .isEqualTo("http://testurl/testimage.jpg");
+    }
+
+    @Test
+    public void getEpisodeContentDescription_blank() {
+        Program program = new ProgramImpl.Builder().build();
+        assertThat(program.getEpisodeContentDescription(RuntimeEnvironment.application)).isNull();
+    }
+
+    @Test
+    public void getEpisodeContentDescription_seasonEpisodeAndTitle() {
+        Program program =
+                new ProgramImpl.Builder()
+                        .setSeasonNumber("1")
+                        .setEpisodeNumber("2")
+                        .setEpisodeTitle("The second one")
+                        .build();
+        assertThat(program.getEpisodeContentDescription(RuntimeEnvironment.application))
+                .isEqualTo("Season 1 Episode 2 The second one");
+    }
+
+    @Test
+    public void getEpisodeContentDescription_EpisodeAndTitle() {
+        Program program =
+                new ProgramImpl.Builder()
+                        .setEpisodeNumber("2")
+                        .setEpisodeTitle("The second one")
+                        .build();
+        assertThat(program.getEpisodeContentDescription(RuntimeEnvironment.application))
+                .isEqualTo("Episode 2 The second one");
+    }
+
+    @Test
+    public void getEpisodeContentDescription_seasonEpisode() {
+        Program program =
+                new ProgramImpl.Builder().setSeasonNumber("1").setEpisodeNumber("2").build();
+        assertThat(program.getEpisodeContentDescription(RuntimeEnvironment.application))
+                .isEqualTo("Season 1 Episode 2 ");
+    }
+
+    @Test
+    public void getEpisodeContentDescription_EpisodeTitle() {
+        Program program = new ProgramImpl.Builder().setEpisodeTitle("The second one").build();
+        assertThat(program.getEpisodeContentDescription(RuntimeEnvironment.application))
+                .isEqualTo("The second one");
+    }
+
+    @Test
+    public void getEpisodeDisplayTitle_blank() {
+        Program program = new ProgramImpl.Builder().build();
+        assertThat(program.getEpisodeDisplayTitle(RuntimeEnvironment.application)).isNull();
+    }
+
+    @Test
+    public void getEpisodeDisplayTitle_seasonEpisodeAndTitle() {
+        Program program =
+                new ProgramImpl.Builder()
+                        .setSeasonNumber("1")
+                        .setEpisodeNumber("2")
+                        .setEpisodeTitle("The second one")
+                        .build();
+        assertThat(program.getEpisodeDisplayTitle(RuntimeEnvironment.application))
+                .isEqualTo("S1: Ep. 2 The second one");
+    }
+
+    @Test
+    public void getEpisodeDisplayTitle_EpisodeTitle() {
+        Program program =
+                new ProgramImpl.Builder()
+                        .setEpisodeNumber("2")
+                        .setEpisodeTitle("The second one")
+                        .build();
+        assertThat(program.getEpisodeDisplayTitle(RuntimeEnvironment.application))
+                .isEqualTo("Ep. 2 The second one");
+    }
+
+    @Test
+    public void getEpisodeDisplayTitle_seasonEpisode() {
+        Program program =
+                new ProgramImpl.Builder().setSeasonNumber("1").setEpisodeNumber("2").build();
+        assertThat(program.getEpisodeDisplayTitle(RuntimeEnvironment.application))
+                .isEqualTo("S1: Ep. 2 ");
+    }
+
+    @Test
+    public void getEpisodeDisplayTitle_episode() {
+        Program program = new ProgramImpl.Builder().setEpisodeTitle("The second one").build();
+        assertThat(program.getEpisodeDisplayTitle(RuntimeEnvironment.application))
+                .isEqualTo("The second one");
+    }
+
+    private static void assertNullCanonicalGenres(Program program) {
+        String[] actual = program.getCanonicalGenres();
+        assertWithMessage("Expected null canonical genres but was " + Arrays.toString(actual))
+                .that(actual)
+                .isNull();
+    }
+
+    private static void assertCanonicalGenres(Program program, String... expected) {
+        assertWithMessage("canonical genres")
+                .that(Arrays.asList(program.getCanonicalGenres()))
+                .isEqualTo(Arrays.asList(expected));
+    }
+
+    private static void assertHasGenre(Program program, int genreId, boolean expected) {
+        assertWithMessage("hasGenre(" + genreId + ")")
+                .that(program.hasGenre(genreId))
+                .isEqualTo(expected);
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/data/TvInputNewComparatorTest.java b/tests/robotests/src/com/android/tv/data/TvInputNewComparatorTest.java
new file mode 100644
index 0000000..bffddf0
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/data/TvInputNewComparatorTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.data;
+
+import android.content.pm.ResolveInfo;
+import android.media.tv.TvInputInfo;
+import android.util.Pair;
+
+import com.android.tv.testing.ComparatorTester;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.android.tv.testing.utils.TestUtils;
+import com.android.tv.util.SetupUtils;
+import com.android.tv.util.TvInputManagerHelper;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+
+/** Test for {@link TvInputNewComparator} */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class TvInputNewComparatorTest {
+    @Test
+    public void testComparator() throws Exception {
+        LinkedHashMap<String, Pair<Boolean, Boolean>> inputIdToNewInput = new LinkedHashMap<>();
+        inputIdToNewInput.put("2_new_input", Pair.create(true, false));
+        inputIdToNewInput.put("4_new_input", Pair.create(true, false));
+        inputIdToNewInput.put("4_old_input", Pair.create(false, false));
+        inputIdToNewInput.put("0_old_input", Pair.create(false, true));
+        inputIdToNewInput.put("1_old_input", Pair.create(false, true));
+        inputIdToNewInput.put("3_old_input", Pair.create(false, true));
+
+        SetupUtils setupUtils = Mockito.mock(SetupUtils.class);
+        Mockito.when(setupUtils.isNewInput(ArgumentMatchers.anyString()))
+                .thenAnswer(
+                        new Answer<Boolean>() {
+                            @Override
+                            public Boolean answer(InvocationOnMock invocation) throws Throwable {
+                                String inputId = (String) invocation.getArguments()[0];
+                                return inputIdToNewInput.get(inputId).first;
+                            }
+                        });
+        Mockito.when(setupUtils.isSetupDone(ArgumentMatchers.anyString()))
+                .thenAnswer(
+                        new Answer<Boolean>() {
+                            @Override
+                            public Boolean answer(InvocationOnMock invocation) throws Throwable {
+                                String inputId = (String) invocation.getArguments()[0];
+                                return inputIdToNewInput.get(inputId).second;
+                            }
+                        });
+        TvInputManagerHelper inputManager = Mockito.mock(TvInputManagerHelper.class);
+        Mockito.when(inputManager.getDefaultTvInputInfoComparator())
+                .thenReturn(
+                        new Comparator<TvInputInfo>() {
+                            @Override
+                            public int compare(TvInputInfo lhs, TvInputInfo rhs) {
+                                return lhs.getId().compareTo(rhs.getId());
+                            }
+                        });
+        TvInputNewComparator comparator = new TvInputNewComparator(setupUtils, inputManager);
+        ComparatorTester<TvInputInfo> comparatorTester =
+                ComparatorTester.withoutEqualsTest(comparator);
+        ResolveInfo resolveInfo = TestUtils.createResolveInfo("test", "test");
+        for (String id : inputIdToNewInput.keySet()) {
+            // Put mock resolveInfo to prevent NPE in {@link TvInputInfo#toString}
+            TvInputInfo info1 =
+                    TestUtils.createTvInputInfo(
+                            resolveInfo, id, "test1", TvInputInfo.TYPE_TUNER, false);
+            TvInputInfo info2 =
+                    TestUtils.createTvInputInfo(
+                            resolveInfo, id, "test2", TvInputInfo.TYPE_DISPLAY_PORT, true);
+            TvInputInfo info3 =
+                    TestUtils.createTvInputInfo(
+                            resolveInfo, id, "test", TvInputInfo.TYPE_HDMI, true);
+            comparatorTester.addComparableGroup(info1, info2, info3);
+        }
+        comparatorTester.test();
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/data/WatchedHistoryManagerTest.java b/tests/robotests/src/com/android/tv/data/WatchedHistoryManagerTest.java
new file mode 100644
index 0000000..9eca7d3
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/data/WatchedHistoryManagerTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.data;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+import com.android.tv.data.WatchedHistoryManager.WatchedRecord;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.google.common.util.concurrent.MoreExecutors;
+import java.util.concurrent.TimeUnit;
+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;
+
+/** Test for {@link WatchedHistoryManagerTest}. */
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class WatchedHistoryManagerTest {
+    // Wait time for expected success.
+    private static final int MAX_HISTORY_SIZE = 100;
+
+    private WatchedHistoryManager mWatchedHistoryManager;
+    private TestWatchedHistoryManagerListener mListener;
+
+    @Before
+    public void setUp() {
+        mWatchedHistoryManager =
+                new WatchedHistoryManager(
+                        RuntimeEnvironment.application,
+                        MAX_HISTORY_SIZE,
+                        MoreExecutors.directExecutor());
+        mListener = new TestWatchedHistoryManagerListener();
+        mWatchedHistoryManager.setListener(mListener);
+    }
+
+    private void startAndWaitForComplete() {
+        mWatchedHistoryManager.start();
+        assertThat(mListener.mLoadFinished).isTrue();
+    }
+
+    @Test
+    public void testIsLoaded() {
+        startAndWaitForComplete();
+        assertThat(mWatchedHistoryManager.isLoaded()).isTrue();
+    }
+
+    @Test
+    public void testLogChannelViewStop() {
+        startAndWaitForComplete();
+        long fakeId = 100000000;
+        long time = System.currentTimeMillis();
+        long duration = TimeUnit.MINUTES.toMillis(10);
+        ChannelImpl channel = new ChannelImpl.Builder().setId(fakeId).build();
+        mWatchedHistoryManager.logChannelViewStop(channel, time, duration);
+
+        WatchedRecord record = mWatchedHistoryManager.getRecord(0);
+        WatchedRecord recordFromSharedPreferences =
+                mWatchedHistoryManager.getRecordFromSharedPreferences(0);
+        assertThat(fakeId).isEqualTo(record.channelId);
+        assertThat(time - duration).isEqualTo(record.watchedStartTime);
+        assertThat(duration).isEqualTo(record.duration);
+        assertThat(recordFromSharedPreferences).isEqualTo(record);
+    }
+
+    @Test
+    public void testCircularHistoryQueue() {
+        startAndWaitForComplete();
+        final long startChannelId = 100000000;
+        long time = System.currentTimeMillis();
+        long duration = TimeUnit.MINUTES.toMillis(10);
+
+        int size = MAX_HISTORY_SIZE * 2;
+        for (int i = 0; i < size; ++i) {
+            ChannelImpl channel = new ChannelImpl.Builder().setId(startChannelId + i).build();
+            mWatchedHistoryManager.logChannelViewStop(channel, time + duration * i, duration);
+        }
+        for (int i = 0; i < MAX_HISTORY_SIZE; ++i) {
+            WatchedRecord record = mWatchedHistoryManager.getRecord(i);
+            WatchedRecord recordFromSharedPreferences =
+                    mWatchedHistoryManager.getRecordFromSharedPreferences(i);
+            assertThat(recordFromSharedPreferences).isEqualTo(record);
+            assertThat(startChannelId + size - 1 - i).isEqualTo(record.channelId);
+        }
+        // Since the WatchedHistory is a circular queue, the value for 0 and maxHistorySize
+        // are same.
+        assertThat(mWatchedHistoryManager.getRecordFromSharedPreferences(MAX_HISTORY_SIZE))
+                .isEqualTo(mWatchedHistoryManager.getRecordFromSharedPreferences(0));
+    }
+
+    @Test
+    public void testWatchedRecordEquals() {
+        assertThat(new WatchedRecord(1, 2, 3).equals(new WatchedRecord(1, 2, 3))).isTrue();
+        assertThat(new WatchedRecord(1, 2, 3).equals(new WatchedRecord(1, 2, 4))).isFalse();
+        assertThat(new WatchedRecord(1, 2, 3).equals(new WatchedRecord(1, 4, 3))).isFalse();
+        assertThat(new WatchedRecord(1, 2, 3).equals(new WatchedRecord(4, 2, 3))).isFalse();
+    }
+
+    @Test
+    public void testEncodeDecodeWatchedRecord() {
+        long fakeId = 100000000;
+        long time = System.currentTimeMillis();
+        long duration = TimeUnit.MINUTES.toMillis(10);
+        WatchedRecord record = new WatchedRecord(fakeId, time, duration);
+        WatchedRecord sameRecord =
+                mWatchedHistoryManager.decode(mWatchedHistoryManager.encode(record));
+        assertThat(sameRecord).isEqualTo(record);
+    }
+
+    private static final class TestWatchedHistoryManagerListener
+            implements WatchedHistoryManager.Listener {
+        boolean mLoadFinished;
+
+        @Override
+        public void onLoadFinished() {
+            mLoadFinished = true;
+        }
+
+        @Override
+        public void onNewRecordAdded(WatchedRecord watchedRecord) {}
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/data/api/ProgramTest.java b/tests/robotests/src/com/android/tv/data/api/ProgramTest.java
new file mode 100644
index 0000000..3b9f062
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/data/api/ProgramTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.data.api;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.tv.data.ProgramImpl;
+import com.android.tv.testing.constants.ConfigConstants;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link Program}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class ProgramTest {
+
+    private static ProgramImpl createProgramWithStartEndTimes(
+            long startTimeUtcMillis, long endTimeUtcMillis) {
+        return new ProgramImpl.Builder()
+                .setStartTimeUtcMillis(startTimeUtcMillis)
+                .setEndTimeUtcMillis(endTimeUtcMillis)
+                .build();
+    }
+
+    private static ProgramImpl createProgramWithChannelId(long channelId) {
+        return new ProgramImpl.Builder().setChannelId(channelId).build();
+    }
+
+    private final Program start10end20 = createProgramWithStartEndTimes(10, 20);
+    private final Program channel1 = createProgramWithChannelId(1);
+
+    @Test
+    public void sameChannel_nullAlwaysFalse() {
+        assertThat(Program.sameChannel(null, null)).isFalse();
+        assertThat(Program.sameChannel(channel1, null)).isFalse();
+        assertThat(Program.sameChannel(null, channel1)).isFalse();
+    }
+
+    @Test
+    public void sameChannel_true() {
+        assertThat(Program.sameChannel(channel1, channel1)).isTrue();
+        assertThat(Program.sameChannel(channel1, createProgramWithChannelId(1))).isTrue();
+    }
+
+    @Test
+    public void sameChannel_false() {
+        assertThat(Program.sameChannel(channel1, createProgramWithChannelId(2))).isFalse();
+    }
+
+    @Test
+    public void isOverLapping_nullAlwaysFalse() {
+        assertThat(Program.isOverlapping(null, null)).isFalse();
+        assertThat(Program.isOverlapping(start10end20, null)).isFalse();
+        assertThat(Program.isOverlapping(null, start10end20)).isFalse();
+    }
+
+    @Test
+    public void isOverLapping_same() {
+        assertThat(Program.isOverlapping(start10end20, start10end20)).isTrue();
+    }
+
+    @Test
+    public void isOverLapping_endBefore() {
+        assertThat(Program.isOverlapping(start10end20, createProgramWithStartEndTimes(1, 9)))
+                .isFalse();
+    }
+
+    @Test
+    public void isOverLapping_endAtStart() {
+        assertThat(Program.isOverlapping(start10end20, createProgramWithStartEndTimes(1, 10)))
+                .isFalse();
+    }
+
+    @Test
+    public void isOverLapping_endDuring() {
+        assertThat(Program.isOverlapping(start10end20, createProgramWithStartEndTimes(1, 11)))
+                .isTrue();
+    }
+
+    @Test
+    public void isOverLapping_startAfter() {
+        assertThat(Program.isOverlapping(start10end20, createProgramWithStartEndTimes(21, 30)))
+                .isFalse();
+    }
+
+    @Test
+    public void isOverLapping_beginAtEnd() {
+        assertThat(Program.isOverlapping(start10end20, createProgramWithStartEndTimes(20, 30)))
+                .isFalse();
+    }
+
+    @Test
+    public void isOverLapping_beginBeforeEnd() {
+        assertThat(Program.isOverlapping(start10end20, createProgramWithStartEndTimes(19, 30)))
+                .isTrue();
+    }
+
+    @Test
+    public void isOverLapping_inside() {
+        assertThat(Program.isOverlapping(start10end20, createProgramWithStartEndTimes(11, 19)))
+                .isTrue();
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/data/epg/EpgFetcherImplTest.java b/tests/robotests/src/com/android/tv/data/epg/EpgFetcherImplTest.java
new file mode 100644
index 0000000..1df89b9
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/data/epg/EpgFetcherImplTest.java
@@ -0,0 +1,305 @@
+/*
+ * 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.data.epg;
+
+/** Tests for {@link EpgFetcher}. */
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.sqlite.SQLiteDatabase;
+import android.media.tv.TvContract;
+import androidx.tvprovider.media.tv.Channel;
+import com.android.tv.common.CommonPreferences;
+import com.android.tv.common.buildtype.HasBuildType.BuildType;
+import com.android.tv.common.flags.impl.DefaultBackendKnobsFlags;
+import com.android.tv.common.flags.impl.SettableFlagsModule;
+import com.android.tv.common.util.PostalCodeUtils;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.features.TvFeatures;
+import com.android.tv.perf.PerformanceMonitor;
+import com.android.tv.perf.stub.StubPerformanceMonitor;
+import com.android.tv.testing.DbTestingUtils;
+import com.android.tv.testing.EpgTestData;
+import com.android.tv.testing.FakeEpgReader;
+import com.android.tv.testing.FakeTvInputManagerHelper;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.android.tv.testing.fakes.FakeClock;
+import com.android.tv.testing.fakes.FakeTvProvider;
+import com.android.tv.testing.robo.ContentProviders;
+import com.google.android.tv.livechannels.epg.provider.EpgContentProvider;
+import com.google.android.tv.partner.support.EpgContract;
+import com.google.common.collect.ImmutableList;
+import dagger.Component;
+import dagger.Module;
+import dagger.android.AndroidInjectionModule;
+import dagger.android.AndroidInjector;
+import dagger.android.DispatchingAndroidInjector;
+import dagger.android.HasAndroidInjector;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import javax.inject.Inject;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.android.util.concurrent.RoboExecutorService;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link EpgFetcherImpl}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK, application = EpgFetcherImplTest.TestApp.class)
+public class EpgFetcherImplTest {
+
+    /** TestApp for {@link EpgFetcherImplTest} */
+    public static class TestApp extends TestSingletonApp implements HasAndroidInjector {
+        @Inject DispatchingAndroidInjector<Object> dispatchingAndroidInjector;
+
+        @Override
+        public void onCreate() {
+            super.onCreate();
+            DaggerEpgFetcherImplTest_TestAppComponent.builder()
+                    .settableFlagsModule(flagsModule)
+                    .build()
+                    .inject(this);
+        }
+
+        @Override
+        public AndroidInjector<Object> androidInjector() {
+            return dispatchingAndroidInjector;
+        }
+    }
+
+    /** Component for {@link EpgFetcherImplTest} */
+    @Component(
+            modules = {
+                AndroidInjectionModule.class,
+                TestModule.class,
+                EpgContentProvider.Module.class
+            })
+    interface TestAppComponent extends AndroidInjector<TestApp> {}
+
+    /** Module for {@link EpgFetcherImplTest} */
+    @Module(includes = {SettableFlagsModule.class})
+    public static class TestModule {}
+
+    private static final String[] PROGRAM_COLUMNS = {
+        TvContract.Programs.COLUMN_CHANNEL_ID,
+        TvContract.Programs.COLUMN_TITLE,
+        TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
+        TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS
+    };
+
+    private static final String[] CHANNEL_COLUMNS = {
+        TvContract.Channels.COLUMN_DISPLAY_NAME,
+        TvContract.Channels.COLUMN_DISPLAY_NUMBER,
+        TvContract.Channels.COLUMN_NETWORK_AFFILIATION
+    };
+
+    private FakeClock mFakeClock;
+    private EpgFetcherImpl mEpgFetcher;
+    private ChannelDataManager mChannelDataManager;
+    private FakeEpgReader mEpgReader;
+    private PerformanceMonitor mPerformanceMonitor = new StubPerformanceMonitor();
+    private ContentResolver mContentResolver;
+    private FakeTvProvider mTvProvider;
+    private EpgContentProvider mEpgProvider;
+    private EpgContentProvider.EpgDatabaseHelper mDatabaseHelper;
+    private TestApp mTestApp;
+
+    @Before
+    public void setup() {
+
+        TvFeatures.CLOUD_EPG_FOR_3RD_PARTY.enableForTest();
+        mTestApp = (TestApp) RuntimeEnvironment.application;
+        Shadows.shadowOf(RuntimeEnvironment.application)
+                .grantPermissions("com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA");
+        mDatabaseHelper = new EpgContentProvider.EpgDatabaseHelper(RuntimeEnvironment.application);
+        CommonPreferences.initialize(RuntimeEnvironment.application);
+        PostalCodeUtils.setLastPostalCode(RuntimeEnvironment.application, "90210");
+        EpgFetchHelper.setLastLineupId(RuntimeEnvironment.application, "test90210");
+        mTvProvider = ContentProviders.register(FakeTvProvider.class, TvContract.AUTHORITY);
+        mEpgProvider = ContentProviders.register(EpgContentProvider.class, EpgContract.AUTHORITY);
+        mEpgProvider.setCallingPackage_("com.google.android.tv");
+        mFakeClock = FakeClock.createWithCurrentTime();
+        FakeTvInputManagerHelper fakeTvInputManagerHelper =
+                new FakeTvInputManagerHelper(RuntimeEnvironment.application);
+        mContentResolver = RuntimeEnvironment.application.getContentResolver();
+        mChannelDataManager =
+                new ChannelDataManager(
+                        RuntimeEnvironment.application,
+                        fakeTvInputManagerHelper,
+                        new RoboExecutorService(),
+                        mContentResolver);
+        fakeTvInputManagerHelper.start();
+        mChannelDataManager.start();
+        mEpgReader = new FakeEpgReader(mFakeClock);
+        mEpgFetcher =
+                new EpgFetcherImpl(
+                        RuntimeEnvironment.application,
+                        new EpgInputWhiteList(
+                                mTestApp.flagsModule.cloudEpgFlags,
+                                mTestApp.flagsModule.legacyFlags),
+                        mChannelDataManager,
+                        mEpgReader,
+                        mPerformanceMonitor,
+                        mFakeClock,
+                        new DefaultBackendKnobsFlags(),
+                        BuildType.NO_JNI_TEST);
+        EpgTestData.DATA_90210.loadData(mFakeClock, mEpgReader); // This also sets fake clock
+        EpgFetchHelper.setLastEpgUpdatedTimestamp(
+                RuntimeEnvironment.application,
+                mFakeClock.currentTimeMillis() - TimeUnit.DAYS.toMillis(1));
+    }
+
+    @After
+    public void after() {
+        mChannelDataManager.stop();
+        TvFeatures.CLOUD_EPG_FOR_3RD_PARTY.resetForTests();
+    }
+
+    @Test
+    public void fetchImmediately_nochannels() throws ExecutionException, InterruptedException {
+        EpgFetcherImpl.FetchAsyncTask fetcherTask = mEpgFetcher.createFetchTask(null, null);
+        fetcherTask.execute();
+
+        assertThat(fetcherTask.get()).isEqualTo(EpgFetcherImpl.REASON_NO_BUILT_IN_CHANNELS);
+        List<List<String>> rows =
+                DbTestingUtils.toList(
+                        mContentResolver.query(
+                                TvContract.Programs.CONTENT_URI,
+                                PROGRAM_COLUMNS,
+                                null,
+                                null,
+                                null));
+        assertThat(rows).isEmpty();
+    }
+
+    @Test
+    public void fetchImmediately_testChannel() throws ExecutionException, InterruptedException {
+        // The channels must be in the app package.
+        // For this test the package is com.android.tv.data.epg
+        insertTestChannels(
+                "com.android.tv.data.epg/.tuner.TunerTvInputService", EpgTestData.CHANNEL_10);
+        EpgFetcherImpl.FetchAsyncTask fetcherTask = mEpgFetcher.createFetchTask(null, null);
+        fetcherTask.execute();
+
+        assertThat(fetcherTask.get()).isNull();
+        List<List<String>> rows =
+                DbTestingUtils.toList(
+                        mContentResolver.query(
+                                TvContract.Programs.CONTENT_URI,
+                                PROGRAM_COLUMNS,
+                                null,
+                                null,
+                                null));
+        assertThat(rows)
+                .containsExactly(
+                        ImmutableList.of("1", "Program 1", "1496358000000", "1496359800000"));
+    }
+
+    @Test
+    public void fetchImmediately_epgChannel() throws ExecutionException, InterruptedException {
+        mTestApp.flagsModule.cloudEpgFlags.setThirdPartyEpgInputCsv("com.example/.Input");
+        insertTestChannels("com.example/.Input", EpgTestData.CHANNEL_10, EpgTestData.CHANNEL_11);
+        createTestEpgInput();
+        EpgFetcherImpl.FetchAsyncTask fetcherTask = mEpgFetcher.createFetchTask(null, null);
+        fetcherTask.execute();
+
+        assertThat(fetcherTask.get()).isNull();
+        List<List<String>> rows =
+                DbTestingUtils.toList(
+                        mContentResolver.query(
+                                TvContract.Programs.CONTENT_URI,
+                                PROGRAM_COLUMNS,
+                                null,
+                                null,
+                                null));
+        assertThat(rows)
+                .containsExactly(
+                        ImmutableList.of("1", "Program 1", "1496358000000", "1496359800000"),
+                        ImmutableList.of("2", "Program 2", "1496359800000", "1496361600000"));
+    }
+
+    @Test
+    public void testUpdateNetworkAffiliation() throws ExecutionException, InterruptedException {
+        if (!TvFeatures.STORE_NETWORK_AFFILIATION.isEnabled(RuntimeEnvironment.application)) {
+            return;
+        }
+        // set network affiliation to null so that it can be updated later
+        Channel channel =
+                new Channel.Builder(EpgTestData.CHANNEL_10).setNetworkAffiliation(null).build();
+        // The channels must be in the app package.
+        // For this test the package is com.android.tv.data.epg
+        insertTestChannels("com.android.tv.data.epg/.tuner.TunerTvInputService", channel);
+
+        List<List<String>> rows =
+                DbTestingUtils.toList(
+                        mContentResolver.query(
+                                TvContract.Channels.CONTENT_URI,
+                                CHANNEL_COLUMNS,
+                                null,
+                                null,
+                                null));
+        assertThat(rows).containsExactly(ImmutableList.of("Channel TEN", "10", "null"));
+        EpgFetcherImpl.FetchAsyncTask fetcherTask = mEpgFetcher.createFetchTask(null, null);
+        fetcherTask.execute();
+
+        assertThat(fetcherTask.get()).isNull();
+        rows =
+                DbTestingUtils.toList(
+                        mContentResolver.query(
+                                TvContract.Channels.CONTENT_URI,
+                                CHANNEL_COLUMNS,
+                                null,
+                                null,
+                                null));
+        // network affiliation should be updated
+        assertThat(rows)
+                .containsExactly(
+                        ImmutableList.of("Channel TEN", "10", "Channel 10 Network Affiliation"));
+    }
+
+    protected void insertTestChannels(String inputId, Channel... channels) {
+
+        for (Channel channel : channels) {
+            ContentValues values =
+                    new Channel.Builder(channel).setInputId(inputId).build().toContentValues();
+            String packageName = inputId.substring(0, inputId.indexOf('/'));
+            mTvProvider.setCallingPackage(packageName);
+            mContentResolver.insert(TvContract.Channels.CONTENT_URI, values);
+            mTvProvider.setCallingPackage("com.android.tv");
+        }
+    }
+
+    private void createTestEpgInput() {
+        // Use the database helper so we can set the package name.
+        SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
+        ContentValues values = new ContentValues();
+        values.put(EpgContract.EpgInputs.COLUMN_ID, "1");
+        values.put(EpgContract.EpgInputs.COLUMN_PACKAGE_NAME, "com.example");
+        values.put(EpgContract.EpgInputs.COLUMN_INPUT_ID, "com.example/.Input");
+        values.put(EpgContract.EpgInputs.COLUMN_LINEUP_ID, "lineup1");
+        long rowId = db.insert("epg_input", null, values);
+        assertThat(rowId).isEqualTo(1);
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/data/epg/EpgInputWhiteListTest.java b/tests/robotests/src/com/android/tv/data/epg/EpgInputWhiteListTest.java
new file mode 100644
index 0000000..c018bd2
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/data/epg/EpgInputWhiteListTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.data.epg;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.tv.common.flags.impl.DefaultCloudEpgFlags;
+import com.android.tv.common.flags.impl.DefaultLegacyFlags;
+import com.android.tv.features.TvFeatures;
+import com.android.tv.testing.constants.ConfigConstants;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link EpgInputWhiteList}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class EpgInputWhiteListTest {
+
+    private EpgInputWhiteList mWhiteList;
+    private DefaultCloudEpgFlags mCloudEpgFlags;
+    private DefaultLegacyFlags mLegacyFlags;
+
+    @Before
+    public void setup() {
+        TvFeatures.CLOUD_EPG_FOR_3RD_PARTY.enableForTest();
+        mCloudEpgFlags = new DefaultCloudEpgFlags();
+        mLegacyFlags = DefaultLegacyFlags.DEFAULT;
+        mWhiteList = new EpgInputWhiteList(mCloudEpgFlags, mLegacyFlags);
+    }
+
+    @After
+    public void after() {
+        TvFeatures.CLOUD_EPG_FOR_3RD_PARTY.resetForTests();
+    }
+
+    @Test
+    public void isInputWhiteListed_noRemoteConfig() {
+        assertThat(mWhiteList.isInputWhiteListed("com.example/.Foo")).isFalse();
+    }
+
+    @Test
+    public void isInputWhiteListed_noMatch() {
+        mCloudEpgFlags.setThirdPartyEpgInputCsv("com.example/.Bar");
+        assertThat(mWhiteList.isInputWhiteListed("com.example/.Foo")).isFalse();
+    }
+
+    @Test
+    public void isInputWhiteListed_match() {
+        mCloudEpgFlags.setThirdPartyEpgInputCsv("com.example/.Foo");
+        assertThat(mWhiteList.isInputWhiteListed("com.example/.Foo")).isTrue();
+    }
+
+    @Test
+    public void isInputWhiteListed_matchWithTwo() {
+        mCloudEpgFlags.setThirdPartyEpgInputCsv("com.example/.Foo,com.example/.Bar");
+        assertThat(mWhiteList.isInputWhiteListed("com.example/.Foo")).isTrue();
+    }
+
+    @Test
+    public void toInputSet_withNewLine() {
+        assertThat(EpgInputWhiteList.toInputSet("com.example/.Foo,\ncom.example/.Bar\n"))
+                .containsExactly("com.example/.Foo", "com.example/.Bar");
+    }
+
+    @Test
+    public void toInputSet_withWhiteSpace() {
+        assertThat(EpgInputWhiteList.toInputSet("com.example/.Foo , com.example/.Bar "))
+                .containsExactly("com.example/.Foo", "com.example/.Bar");
+    }
+
+    @Test
+    public void isPackageWhiteListed_noRemoteConfig() {
+        assertThat(mWhiteList.isPackageWhiteListed("com.example")).isFalse();
+    }
+
+    @Test
+    public void isPackageWhiteListed_noMatch() {
+        mCloudEpgFlags.setThirdPartyEpgInputCsv("com.example/.Bar");
+        assertThat(mWhiteList.isPackageWhiteListed("com.other")).isFalse();
+    }
+
+    @Test
+    public void isPackageWhiteListed_match() {
+        mCloudEpgFlags.setThirdPartyEpgInputCsv("com.example/.Foo");
+        assertThat(mWhiteList.isPackageWhiteListed("com.example")).isTrue();
+    }
+
+    @Test
+    public void isPackageWhiteListed_matchWithTwo() {
+        mCloudEpgFlags.setThirdPartyEpgInputCsv("com.example/.Foo,com.example/.Bar");
+        assertThat(mWhiteList.isPackageWhiteListed("com.example")).isTrue();
+    }
+
+    @Test
+    public void isPackageWhiteListed_matchBadInput() {
+        mCloudEpgFlags.setThirdPartyEpgInputCsv("com.example.Foo");
+        assertThat(mWhiteList.isPackageWhiteListed("com.example")).isFalse();
+    }
+
+    @Test
+    public void isPackageWhiteListed_tunerInput() {
+        EpgInputWhiteList whiteList =
+                new EpgInputWhiteList(new DefaultCloudEpgFlags(), DefaultLegacyFlags.DEFAULT);
+        assertThat(
+                        whiteList.isInputWhiteListed(
+                                "com.google.android.tv/.tuner.tvinput.TunerTvInputService"))
+                .isTrue();
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/dvr/BaseDvrDataManagerTest.java b/tests/robotests/src/com/android/tv/dvr/BaseDvrDataManagerTest.java
new file mode 100644
index 0000000..636bf2d
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/dvr/BaseDvrDataManagerTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr;
+
+import android.os.Build;
+import android.support.annotation.NonNull;
+import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.feature.TestableFeature;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.dvr.DvrDataManagerInMemoryImpl;
+import com.android.tv.testing.dvr.RecordingTestUtils;
+import com.android.tv.testing.fakes.FakeClock;
+import com.google.common.truth.Truth;
+import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link BaseDvrDataManager} using {@link DvrDataManagerInMemoryImpl}. */
+@RunWith(GoogleRobolectricTestRunner.class)
+@Config(sdk = Build.VERSION_CODES.N, application = TestSingletonApp.class)
+public class BaseDvrDataManagerTest {
+    private static final String INPUT_ID = "input_id";
+    private static final int CHANNEL_ID = 273;
+
+    private final TestableFeature mDvrFeature = CommonFeatures.DVR;
+    private DvrDataManagerInMemoryImpl mDvrDataManager;
+    private FakeClock mFakeClock;
+
+    @Before
+    public void setUp() {
+        mDvrFeature.enableForTest();
+        mFakeClock = FakeClock.createWithCurrentTime();
+        mDvrDataManager =
+                new DvrDataManagerInMemoryImpl(RuntimeEnvironment.application, mFakeClock);
+    }
+
+    @After
+    public void tearDown() {
+        mDvrFeature.resetForTests();
+    }
+
+    @Test
+    public void testGetNonStartedScheduledRecordings() {
+        ScheduledRecording recording =
+                mDvrDataManager.addScheduledRecordingInternal(
+                        createNewScheduledRecordingStartingNow());
+        List<ScheduledRecording> result = mDvrDataManager.getNonStartedScheduledRecordings();
+        Truth.assertThat(result).containsExactly(recording);
+    }
+
+    @Test
+    public void testGetNonStartedScheduledRecordings_past() {
+        mDvrDataManager.addScheduledRecordingInternal(createNewScheduledRecordingStartingNow());
+        mFakeClock.increment(TimeUnit.MINUTES, 6);
+        List<ScheduledRecording> result = mDvrDataManager.getNonStartedScheduledRecordings();
+        Truth.assertThat(result).isEmpty();
+    }
+
+    @NonNull
+    private ScheduledRecording createNewScheduledRecordingStartingNow() {
+        return ScheduledRecording.buildFrom(
+                        RecordingTestUtils.createTestRecordingWithIdAndPeriod(
+                                ScheduledRecording.ID_NOT_SET,
+                                INPUT_ID,
+                                CHANNEL_ID,
+                                mFakeClock.currentTimeMillis(),
+                                mFakeClock.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5)))
+                .setState(ScheduledRecording.STATE_RECORDING_NOT_STARTED)
+                .build();
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/dvr/DvrDataManagerImplTest.java b/tests/robotests/src/com/android/tv/dvr/DvrDataManagerImplTest.java
new file mode 100644
index 0000000..8291c3a
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/dvr/DvrDataManagerImplTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.dvr;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.os.Build;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.dvr.RecordingTestUtils;
+import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link DvrDataManagerImpl} */
+@RunWith(GoogleRobolectricTestRunner.class)
+@Config(sdk = Build.VERSION_CODES.N, application = TestSingletonApp.class)
+public class DvrDataManagerImplTest {
+    private static final String INPUT_ID = "input_id";
+    private static final int CHANNEL_ID = 273;
+
+    @Test
+    public void testGetNextScheduledStartTimeAfter() {
+        long id = 1;
+        List<ScheduledRecording> scheduledRecordings = new ArrayList<>();
+        assertNextStartTime(scheduledRecordings, 0L, DvrDataManager.NEXT_START_TIME_NOT_FOUND);
+        scheduledRecordings.add(
+                RecordingTestUtils.createTestRecordingWithIdAndPeriod(
+                        id++, INPUT_ID, CHANNEL_ID, 10L, 20L));
+        assertNextStartTime(scheduledRecordings, 9L, 10L);
+        assertNextStartTime(scheduledRecordings, 10L, DvrDataManager.NEXT_START_TIME_NOT_FOUND);
+        scheduledRecordings.add(
+                RecordingTestUtils.createTestRecordingWithIdAndPeriod(
+                        id++, INPUT_ID, CHANNEL_ID, 20L, 30L));
+        assertNextStartTime(scheduledRecordings, 9L, 10L);
+        assertNextStartTime(scheduledRecordings, 10L, 20L);
+        assertNextStartTime(scheduledRecordings, 20L, DvrDataManager.NEXT_START_TIME_NOT_FOUND);
+        scheduledRecordings.add(
+                RecordingTestUtils.createTestRecordingWithIdAndPeriod(
+                        id++, INPUT_ID, CHANNEL_ID, 30L, 40L));
+        assertNextStartTime(scheduledRecordings, 9L, 10L);
+        assertNextStartTime(scheduledRecordings, 10L, 20L);
+        assertNextStartTime(scheduledRecordings, 20L, 30L);
+        assertNextStartTime(scheduledRecordings, 30L, DvrDataManager.NEXT_START_TIME_NOT_FOUND);
+        scheduledRecordings.clear();
+        scheduledRecordings.add(
+                RecordingTestUtils.createTestRecordingWithIdAndPeriod(
+                        id++, INPUT_ID, CHANNEL_ID, 10L, 20L));
+        scheduledRecordings.add(
+                RecordingTestUtils.createTestRecordingWithIdAndPeriod(
+                        id++, INPUT_ID, CHANNEL_ID, 10L, 20L));
+        scheduledRecordings.add(
+                RecordingTestUtils.createTestRecordingWithIdAndPeriod(
+                        id++, INPUT_ID, CHANNEL_ID, 10L, 20L));
+        assertNextStartTime(scheduledRecordings, 9L, 10L);
+        assertNextStartTime(scheduledRecordings, 10L, DvrDataManager.NEXT_START_TIME_NOT_FOUND);
+    }
+
+    private void assertNextStartTime(
+            List<ScheduledRecording> scheduledRecordings, long startTime, long expected) {
+        assertWithMessage("getNextScheduledStartTimeAfter()")
+                .that(DvrDataManagerImpl.getNextStartTimeAfter(scheduledRecordings, startTime))
+                .isEqualTo(expected);
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/dvr/DvrScheduleManagerTest.java b/tests/robotests/src/com/android/tv/dvr/DvrScheduleManagerTest.java
new file mode 100644
index 0000000..d0a58c9
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/dvr/DvrScheduleManagerTest.java
@@ -0,0 +1,879 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.dvr;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import android.os.Build;
+import android.util.Range;
+import com.android.tv.dvr.DvrScheduleManager.ConflictInfo;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.dvr.RecordingTestUtils;
+import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link DvrScheduleManager} */
+@RunWith(GoogleRobolectricTestRunner.class)
+@Config(sdk = Build.VERSION_CODES.N, application = TestSingletonApp.class)
+public class DvrScheduleManagerTest {
+    private static final String INPUT_ID = "input_id";
+
+    @Test
+    public void testGetConflictingSchedules_emptySchedule() {
+        List<ScheduledRecording> schedules = new ArrayList<>();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 1)).isEmpty();
+    }
+
+    @Test
+    public void testGetConflictingSchedules_noConflict() {
+        long priority = 0;
+        long channelId = 0;
+        List<ScheduledRecording> schedules = new ArrayList<>();
+
+        schedules.add(
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 200L));
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 1)).isEmpty();
+
+        schedules.add(
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 100L));
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 2)).isEmpty();
+
+        schedules.add(
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 100L, 200L));
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 2)).isEmpty();
+
+        schedules.add(
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 100L));
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 3)).isEmpty();
+
+        schedules.add(
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 100L, 200L));
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 3)).isEmpty();
+    }
+
+    @Test
+    public void testGetConflictingSchedules_noTuner() {
+        long priority = 0;
+        long channelId = 0;
+        List<ScheduledRecording> schedules = new ArrayList<>();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 0)).isEmpty();
+
+        schedules.add(
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 200L));
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 0)).isEqualTo(schedules);
+        schedules.add(
+                0,
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 100L));
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 0)).isEqualTo(schedules);
+    }
+
+    @Test
+    public void testGetConflictingSchedules_conflict() {
+        long priority = 0;
+        long channelId = 0;
+        List<ScheduledRecording> schedules = new ArrayList<>();
+
+        ScheduledRecording r1 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 200L);
+        schedules.add(r1);
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 1)).isEmpty();
+
+        ScheduledRecording r2 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 100L);
+        schedules.add(r2);
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 1))
+                .containsExactly(r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 2)).isEmpty();
+
+        ScheduledRecording r3 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 100L, 200L);
+        schedules.add(r3);
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 1))
+                .containsExactly(r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 2)).isEmpty();
+
+        ScheduledRecording r4 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 100L);
+        schedules.add(r4);
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 1))
+                .containsExactly(r2, r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 2))
+                .containsExactly(r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 3)).isEmpty();
+
+        ScheduledRecording r5 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 100L, 200L);
+        schedules.add(r5);
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 1))
+                .containsExactly(r3, r2, r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 2))
+                .containsExactly(r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 3)).isEmpty();
+
+        ScheduledRecording r6 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 10L, 90L);
+        schedules.add(r6);
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 1))
+                .containsExactly(r4, r3, r2, r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 2))
+                .containsExactly(r2, r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 3))
+                .containsExactly(r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 4)).isEmpty();
+
+        ScheduledRecording r7 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 110L, 190L);
+        schedules.add(r7);
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 1))
+                .containsExactly(r5, r4, r3, r2, r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 2))
+                .containsExactly(r3, r2, r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 3))
+                .containsExactly(r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 4)).isEmpty();
+
+        ScheduledRecording r8 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 50L, 150L);
+        schedules.add(r8);
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 1))
+                .containsExactly(r7, r6, r5, r4, r3, r2, r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 2))
+                .containsExactly(r5, r4, r3, r2, r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 3))
+                .containsExactly(r3, r2, r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 4))
+                .containsExactly(r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 5)).isEmpty();
+    }
+
+    @Test
+    public void testGetConflictingSchedules_conflict2() {
+        // The case when there is a long schedule.
+        long priority = 0;
+        long channelId = 0;
+        List<ScheduledRecording> schedules = new ArrayList<>();
+
+        ScheduledRecording r1 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 1000L);
+        schedules.add(r1);
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 1)).isEmpty();
+
+        ScheduledRecording r2 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 100L);
+        schedules.add(r2);
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 1))
+                .containsExactly(r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 2)).isEmpty();
+
+        ScheduledRecording r3 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 100L, 200L);
+        schedules.add(r3);
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 1))
+                .containsExactly(r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 2)).isEmpty();
+    }
+
+    @Test
+    public void testGetConflictingSchedules_reverseOrder() {
+        long priority = 0;
+        long channelId = 0;
+        List<ScheduledRecording> schedules = new ArrayList<>();
+
+        ScheduledRecording r1 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 200L);
+        schedules.add(0, r1);
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 1)).isEmpty();
+
+        ScheduledRecording r2 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 100L);
+        schedules.add(0, r2);
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 1))
+                .containsExactly(r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 2)).isEmpty();
+
+        ScheduledRecording r3 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 100L, 200L);
+        schedules.add(0, r3);
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 1))
+                .containsExactly(r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 2)).isEmpty();
+
+        ScheduledRecording r4 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 100L);
+        schedules.add(0, r4);
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 1))
+                .containsExactly(r2, r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 2))
+                .containsExactly(r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 3)).isEmpty();
+
+        ScheduledRecording r5 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 100L, 200L);
+        schedules.add(0, r5);
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 1))
+                .containsExactly(r3, r2, r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 2))
+                .containsExactly(r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 3)).isEmpty();
+
+        ScheduledRecording r6 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 10L, 90L);
+        schedules.add(0, r6);
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 1))
+                .containsExactly(r4, r3, r2, r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 2))
+                .containsExactly(r2, r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 3))
+                .containsExactly(r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 4)).isEmpty();
+
+        ScheduledRecording r7 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 110L, 190L);
+        schedules.add(0, r7);
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 1))
+                .containsExactly(r5, r4, r3, r2, r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 2))
+                .containsExactly(r3, r2, r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 3))
+                .containsExactly(r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 4)).isEmpty();
+
+        ScheduledRecording r8 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 50L, 150L);
+        schedules.add(0, r8);
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 1))
+                .containsExactly(r7, r6, r5, r4, r3, r2, r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 2))
+                .containsExactly(r5, r4, r3, r2, r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 3))
+                .containsExactly(r3, r2, r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 4))
+                .containsExactly(r1)
+                .inOrder();
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 5)).isEmpty();
+    }
+
+    @Test
+    public void testGetConflictingSchedules_period1() {
+        long priority = 0;
+        long channelId = 0;
+        List<ScheduledRecording> schedules = new ArrayList<>();
+
+        ScheduledRecording r1 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 200L);
+        schedules.add(r1);
+        ScheduledRecording r2 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 100L);
+        schedules.add(r2);
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedules(
+                                schedules, 1, Collections.singletonList(new Range<>(10L, 20L))))
+                .containsExactly(r1)
+                .inOrder();
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedules(
+                                schedules, 1, Collections.singletonList(new Range<>(110L, 120L))))
+                .containsExactly(r1)
+                .inOrder();
+    }
+
+    @Test
+    public void testGetConflictingSchedules_period2() {
+        long priority = 0;
+        long channelId = 0;
+        List<ScheduledRecording> schedules = new ArrayList<>();
+
+        ScheduledRecording r1 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 200L);
+        schedules.add(r1);
+        ScheduledRecording r2 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 100L, 200L);
+        schedules.add(r2);
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedules(
+                                schedules, 1, Collections.singletonList(new Range<>(10L, 20L))))
+                .containsExactly(r1)
+                .inOrder();
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedules(
+                                schedules, 1, Collections.singletonList(new Range<>(110L, 120L))))
+                .containsExactly(r1)
+                .inOrder();
+    }
+
+    @Test
+    public void testGetConflictingSchedules_period3() {
+        long priority = 0;
+        long channelId = 0;
+        List<ScheduledRecording> schedules = new ArrayList<>();
+
+        ScheduledRecording r1 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 100L);
+        schedules.add(r1);
+        ScheduledRecording r2 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 100L, 200L);
+        schedules.add(r2);
+        ScheduledRecording r3 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 100L);
+        schedules.add(r3);
+        ScheduledRecording r4 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 100L, 200L);
+        schedules.add(r4);
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedules(
+                                schedules, 1, Collections.singletonList(new Range<>(10L, 20L))))
+                .containsExactly(r1)
+                .inOrder();
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedules(
+                                schedules, 1, Collections.singletonList(new Range<>(110L, 120L))))
+                .containsExactly(r2)
+                .inOrder();
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedules(
+                                schedules, 1, Collections.singletonList(new Range<>(50L, 150L))))
+                .containsExactly(r2, r1)
+                .inOrder();
+        List<Range<Long>> ranges = new ArrayList<>();
+        ranges.add(new Range<>(10L, 20L));
+        ranges.add(new Range<>(110L, 120L));
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 1, ranges))
+                .containsExactly(r2, r1)
+                .inOrder();
+    }
+
+    @Test
+    public void testGetConflictingSchedules_addSchedules1() {
+        long priority = 0;
+        long channelId = 0;
+        List<ScheduledRecording> schedules = new ArrayList<>();
+
+        ScheduledRecording r1 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 200L);
+        schedules.add(r1);
+        ScheduledRecording r2 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 100L);
+        schedules.add(r2);
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedules(
+                                Collections.singletonList(
+                                        ScheduledRecording.builder(INPUT_ID, ++channelId, 10L, 20L)
+                                                .setPriority(++priority)
+                                                .build()),
+                                schedules,
+                                1))
+                .containsExactly(r2, r1)
+                .inOrder();
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedules(
+                                Collections.singletonList(
+                                        ScheduledRecording.builder(
+                                                        INPUT_ID, ++channelId, 110L, 120L)
+                                                .setPriority(++priority)
+                                                .build()),
+                                schedules,
+                                1))
+                .containsExactly(r1)
+                .inOrder();
+    }
+
+    @Test
+    public void testGetConflictingSchedules_addSchedules2() {
+        long priority = 0;
+        long channelId = 0;
+        List<ScheduledRecording> schedules = new ArrayList<>();
+
+        ScheduledRecording r1 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 200L);
+        schedules.add(r1);
+        ScheduledRecording r2 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 100L, 200L);
+        schedules.add(r2);
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedules(
+                                Collections.singletonList(
+                                        ScheduledRecording.builder(INPUT_ID, ++channelId, 10L, 20L)
+                                                .setPriority(++priority)
+                                                .build()),
+                                schedules,
+                                1))
+                .containsExactly(r1)
+                .inOrder();
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedules(
+                                Collections.singletonList(
+                                        ScheduledRecording.builder(
+                                                        INPUT_ID, ++channelId, 110L, 120L)
+                                                .setPriority(++priority)
+                                                .build()),
+                                schedules,
+                                1))
+                .containsExactly(r2, r1)
+                .inOrder();
+    }
+
+    @Test
+    public void testGetConflictingSchedules_addLowestPriority() {
+        long priority = 0;
+        long channelId = 0;
+        List<ScheduledRecording> schedules = new ArrayList<>();
+
+        ScheduledRecording r1 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 400L);
+        schedules.add(r1);
+        ScheduledRecording r2 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 100L, 200L);
+        schedules.add(r2);
+        // Returning r1 even though r1 has the higher priority than the new one. That's because r1
+        // starts at 0 and stops at 100, and the new one will be recorded successfully.
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedules(
+                                Collections.singletonList(
+                                        ScheduledRecording.builder(
+                                                        INPUT_ID, ++channelId, 200L, 300L)
+                                                .setPriority(0)
+                                                .build()),
+                                schedules,
+                                1))
+                .containsExactly(r1)
+                .inOrder();
+    }
+
+    @Test
+    public void testGetConflictingSchedules_sameChannel() {
+        long priority = 0;
+        long channelId = 1;
+        List<ScheduledRecording> schedules = new ArrayList<>();
+        schedules.add(
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        channelId, ++priority, 0L, 200L));
+        schedules.add(
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        channelId, ++priority, 0L, 200L));
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 3)).isEmpty();
+    }
+
+    @Test
+    public void testGetConflictingSchedule_startEarlyAndFail() {
+        long priority = 0;
+        long channelId = 0;
+        List<ScheduledRecording> schedules = new ArrayList<>();
+        ScheduledRecording r1 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 200L, 300L);
+        schedules.add(r1);
+        ScheduledRecording r2 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 400L);
+        schedules.add(r2);
+        ScheduledRecording r3 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 100L, 200L);
+        schedules.add(r3);
+        // r2 starts recording and fails when r3 starts. r1 is recorded successfully.
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 1))
+                .containsExactly(r2)
+                .inOrder();
+    }
+
+    @Test
+    public void testGetConflictingSchedule_startLate() {
+        long priority = 0;
+        long channelId = 0;
+        List<ScheduledRecording> schedules = new ArrayList<>();
+        ScheduledRecording r1 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 200L, 400L);
+        schedules.add(r1);
+        ScheduledRecording r2 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 100L, 300L);
+        schedules.add(r2);
+        ScheduledRecording r3 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 200L);
+        schedules.add(r3);
+        // r2 and r1 are clipped.
+        assertThat(DvrScheduleManager.getConflictingSchedules(schedules, 1))
+                .containsExactly(r2, r1)
+                .inOrder();
+    }
+
+    @Test
+    public void testGetConflictingSchedulesForTune_canTune() {
+        // Can tune to the recorded channel if tuner count is 1.
+        long priority = 0;
+        long channelId = 1;
+        List<ScheduledRecording> schedules = new ArrayList<>();
+        schedules.add(
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        channelId, ++priority, 0L, 200L));
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedulesForTune(
+                                INPUT_ID, channelId, 0L, priority + 1, schedules, 1))
+                .isEmpty();
+    }
+
+    @Test
+    public void testGetConflictingSchedulesForTune_cannotTune() {
+        // Can't tune to a channel if other channel is recording and tuner count is 1.
+        long priority = 0;
+        long channelId = 1;
+        List<ScheduledRecording> schedules = new ArrayList<>();
+        schedules.add(
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        channelId, ++priority, 0L, 200L));
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedulesForTune(
+                                INPUT_ID, channelId + 1, 0L, priority + 1, schedules, 1))
+                .containsExactly(schedules.get(0))
+                .inOrder();
+    }
+
+    @Test
+    public void testGetConflictingSchedulesForWatching_otherChannels() {
+        // The other channels are to be recorded.
+        long priority = 0;
+        long channelToWatch = 1;
+        long channelId = 1;
+        List<ScheduledRecording> schedules = new ArrayList<>();
+        ScheduledRecording r1 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 200L);
+        schedules.add(r1);
+        ScheduledRecording r2 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 200L);
+        schedules.add(r2);
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedulesForWatching(
+                                INPUT_ID, channelToWatch, 0L, ++priority, schedules, 3))
+                .isEmpty();
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedulesForWatching(
+                                INPUT_ID, channelToWatch, 0L, ++priority, schedules, 2))
+                .containsExactly(r1)
+                .inOrder();
+    }
+
+    @Test
+    public void testGetConflictingSchedulesForWatching_sameChannel1() {
+        long priority = 0;
+        long channelToWatch = 1;
+        long channelId = 1;
+        List<ScheduledRecording> schedules = new ArrayList<>();
+        ScheduledRecording r1 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        channelToWatch, ++priority, 0L, 200L);
+        schedules.add(r1);
+        ScheduledRecording r2 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 200L);
+        schedules.add(r2);
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedulesForWatching(
+                                INPUT_ID, channelToWatch, 0L, ++priority, schedules, 2))
+                .isEmpty();
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedulesForWatching(
+                                INPUT_ID, channelToWatch, 0L, ++priority, schedules, 1))
+                .containsExactly(r2)
+                .inOrder();
+    }
+
+    @Test
+    public void testGetConflictingSchedulesForWatching_sameChannel2() {
+        long priority = 0;
+        long channelToWatch = 1;
+        long channelId = 1;
+        List<ScheduledRecording> schedules = new ArrayList<>();
+        ScheduledRecording r1 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 200L);
+        schedules.add(r1);
+        ScheduledRecording r2 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        channelToWatch, ++priority, 0L, 200L);
+        schedules.add(r2);
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedulesForWatching(
+                                INPUT_ID, channelToWatch, 0L, ++priority, schedules, 2))
+                .isEmpty();
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedulesForWatching(
+                                INPUT_ID, channelToWatch, 0L, ++priority, schedules, 1))
+                .containsExactly(r1)
+                .inOrder();
+    }
+
+    @Test
+    public void testGetConflictingSchedulesForWatching_sameChannelConflict1() {
+        long priority = 0;
+        long channelToWatch = 1;
+        long channelId = 1;
+        List<ScheduledRecording> schedules = new ArrayList<>();
+        ScheduledRecording r1 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 200L);
+        schedules.add(r1);
+        ScheduledRecording r2 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        channelToWatch, ++priority, 0L, 200L);
+        schedules.add(r2);
+        ScheduledRecording r3 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        channelToWatch, ++priority, 0L, 200L);
+        schedules.add(r3);
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedulesForWatching(
+                                INPUT_ID, channelToWatch, 0L, ++priority, schedules, 3))
+                .containsExactly(r2)
+                .inOrder();
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedulesForWatching(
+                                INPUT_ID, channelToWatch, 0L, ++priority, schedules, 2))
+                .containsExactly(r2)
+                .inOrder();
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedulesForWatching(
+                                INPUT_ID, channelToWatch, 0L, ++priority, schedules, 1))
+                .containsExactly(r2, r1)
+                .inOrder();
+    }
+
+    @Test
+    public void testGetConflictingSchedulesForWatching_sameChannelConflict2() {
+        long priority = 0;
+        long channelToWatch = 1;
+        long channelId = 1;
+        List<ScheduledRecording> schedules = new ArrayList<>();
+        ScheduledRecording r1 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        channelToWatch, ++priority, 0L, 200L);
+        schedules.add(r1);
+        ScheduledRecording r2 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        channelToWatch, ++priority, 0L, 200L);
+        schedules.add(r2);
+        ScheduledRecording r3 =
+                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                        ++channelId, ++priority, 0L, 200L);
+        schedules.add(r3);
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedulesForWatching(
+                                INPUT_ID, channelToWatch, 0L, ++priority, schedules, 3))
+                .containsExactly(r1)
+                .inOrder();
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedulesForWatching(
+                                INPUT_ID, channelToWatch, 0L, ++priority, schedules, 2))
+                .containsExactly(r1)
+                .inOrder();
+        assertThat(
+                        DvrScheduleManager.getConflictingSchedulesForWatching(
+                                INPUT_ID, channelToWatch, 0L, ++priority, schedules, 1))
+                .containsExactly(r3, r1)
+                .inOrder();
+    }
+
+    @Test
+    public void testPartiallyConflictingSchedules() {
+        long priority = 100;
+        long channelId = 0;
+        List<ScheduledRecording> schedules =
+                new ArrayList<>(
+                        Arrays.asList(
+                                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                                        ++channelId, --priority, 0L, 400L),
+                                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                                        ++channelId, --priority, 0L, 200L),
+                                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                                        ++channelId, --priority, 200L, 500L),
+                                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                                        ++channelId, --priority, 400L, 600L),
+                                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                                        ++channelId, --priority, 700L, 800L),
+                                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                                        ++channelId, --priority, 600L, 900L),
+                                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                                        ++channelId, --priority, 800L, 900L),
+                                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                                        ++channelId, --priority, 800L, 900L),
+                                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                                        ++channelId, --priority, 750L, 850L),
+                                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                                        ++channelId, --priority, 300L, 450L),
+                                RecordingTestUtils.createTestRecordingWithPriorityAndPeriod(
+                                        ++channelId, --priority, 50L, 900L)));
+        List<ConflictInfo> conflicts = DvrScheduleManager.getConflictingSchedulesInfo(schedules, 1);
+
+        assertNotInList(schedules.get(0), conflicts);
+        assertFullConflict(schedules.get(1), conflicts);
+        assertPartialConflict(schedules.get(2), conflicts);
+        assertPartialConflict(schedules.get(3), conflicts);
+        assertNotInList(schedules.get(4), conflicts);
+        assertPartialConflict(schedules.get(5), conflicts);
+        assertNotInList(schedules.get(6), conflicts);
+        assertFullConflict(schedules.get(7), conflicts);
+        assertFullConflict(schedules.get(8), conflicts);
+        assertFullConflict(schedules.get(9), conflicts);
+        assertFullConflict(schedules.get(10), conflicts);
+
+        conflicts = DvrScheduleManager.getConflictingSchedulesInfo(schedules, 2);
+
+        assertNotInList(schedules.get(0), conflicts);
+        assertNotInList(schedules.get(1), conflicts);
+        assertNotInList(schedules.get(2), conflicts);
+        assertNotInList(schedules.get(3), conflicts);
+        assertNotInList(schedules.get(4), conflicts);
+        assertNotInList(schedules.get(5), conflicts);
+        assertNotInList(schedules.get(6), conflicts);
+        assertFullConflict(schedules.get(7), conflicts);
+        assertFullConflict(schedules.get(8), conflicts);
+        assertFullConflict(schedules.get(9), conflicts);
+        assertPartialConflict(schedules.get(10), conflicts);
+
+        conflicts = DvrScheduleManager.getConflictingSchedulesInfo(schedules, 3);
+
+        assertNotInList(schedules.get(0), conflicts);
+        assertNotInList(schedules.get(1), conflicts);
+        assertNotInList(schedules.get(2), conflicts);
+        assertNotInList(schedules.get(3), conflicts);
+        assertNotInList(schedules.get(4), conflicts);
+        assertNotInList(schedules.get(5), conflicts);
+        assertNotInList(schedules.get(6), conflicts);
+        assertNotInList(schedules.get(7), conflicts);
+        assertPartialConflict(schedules.get(8), conflicts);
+        assertNotInList(schedules.get(9), conflicts);
+        assertPartialConflict(schedules.get(10), conflicts);
+    }
+
+    private void assertNotInList(ScheduledRecording schedule, List<ConflictInfo> conflicts) {
+        for (ConflictInfo conflictInfo : conflicts) {
+            if (conflictInfo.schedule.equals(schedule)) {
+                fail(schedule + " conflicts with others.");
+            }
+        }
+    }
+
+    private void assertPartialConflict(ScheduledRecording schedule, List<ConflictInfo> conflicts) {
+        for (ConflictInfo conflictInfo : conflicts) {
+            if (conflictInfo.schedule.equals(schedule)) {
+                if (conflictInfo.partialConflict) {
+                    return;
+                } else {
+                    fail(schedule + " fully conflicts with others.");
+                }
+            }
+        }
+        fail(schedule + " doesn't conflict");
+    }
+
+    private void assertFullConflict(ScheduledRecording schedule, List<ConflictInfo> conflicts) {
+        for (ConflictInfo conflictInfo : conflicts) {
+            if (conflictInfo.schedule.equals(schedule)) {
+                if (!conflictInfo.partialConflict) {
+                    return;
+                } else {
+                    fail(schedule + " partially conflicts with others.");
+                }
+            }
+        }
+        fail(schedule + " doesn't conflict");
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/dvr/ScheduledRecordingTest.java b/tests/robotests/src/com/android/tv/dvr/ScheduledRecordingTest.java
new file mode 100644
index 0000000..8213fb3
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/dvr/ScheduledRecordingTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.dvr;
+
+import static com.android.tv.testing.dvr.RecordingTestUtils.createTestRecordingWithIdAndPeriod;
+import static com.android.tv.testing.dvr.RecordingTestUtils.normalizePriority;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.os.Build;
+import android.util.Range;
+
+import com.android.tv.data.ChannelImpl;
+import com.android.tv.data.ProgramImpl;
+import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.dvr.RecordingTestUtils;
+
+import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/** Tests for {@link ScheduledRecordingTest} */
+@RunWith(GoogleRobolectricTestRunner.class)
+@Config(sdk = Build.VERSION_CODES.N, application = TestSingletonApp.class)
+public class ScheduledRecordingTest {
+    private static final String INPUT_ID = "input_id";
+    private static final int CHANNEL_ID = 273;
+
+    @Test
+    public void testIsOverLapping() {
+        ScheduledRecording r =
+                createTestRecordingWithIdAndPeriod(1, INPUT_ID, CHANNEL_ID, 10L, 20L);
+        assertOverLapping(false, 1L, 9L, r);
+
+        assertOverLapping(true, 1L, 20L, r);
+        assertOverLapping(false, 1L, 10L, r);
+        assertOverLapping(true, 10L, 19L, r);
+        assertOverLapping(true, 10L, 20L, r);
+        assertOverLapping(true, 11L, 20L, r);
+        assertOverLapping(true, 11L, 21L, r);
+        assertOverLapping(false, 20L, 21L, r);
+
+        assertOverLapping(false, 21L, 29L, r);
+    }
+
+    @Test
+    public void testBuildProgram() {
+        Channel c = new ChannelImpl.Builder().build();
+        Program p = new ProgramImpl.Builder().build();
+        ScheduledRecording actual =
+                ScheduledRecording.builder(INPUT_ID, p).setChannelId(c.getId()).build();
+        assertWithMessage("type").that(actual.getType()).isEqualTo(ScheduledRecording.TYPE_PROGRAM);
+    }
+
+    @Test
+    public void testBuildTime() {
+        ScheduledRecording actual =
+                createTestRecordingWithIdAndPeriod(1, INPUT_ID, CHANNEL_ID, 10L, 20L);
+        assertWithMessage("type").that(actual.getType()).isEqualTo(ScheduledRecording.TYPE_TIMED);
+    }
+
+    @Test
+    public void testBuildFrom() {
+        ScheduledRecording expected =
+                createTestRecordingWithIdAndPeriod(1, INPUT_ID, CHANNEL_ID, 10L, 20L);
+        ScheduledRecording actual = ScheduledRecording.buildFrom(expected).build();
+        RecordingTestUtils.assertRecordingEquals(expected, actual);
+    }
+
+    @Test
+    public void testBuild_priority() {
+        ScheduledRecording a =
+                normalizePriority(
+                        createTestRecordingWithIdAndPeriod(1, INPUT_ID, CHANNEL_ID, 10L, 20L));
+        ScheduledRecording b =
+                normalizePriority(
+                        createTestRecordingWithIdAndPeriod(2, INPUT_ID, CHANNEL_ID, 10L, 20L));
+        ScheduledRecording c =
+                normalizePriority(
+                        createTestRecordingWithIdAndPeriod(3, INPUT_ID, CHANNEL_ID, 10L, 20L));
+
+        // default priority
+        assertThat(sortByPriority(c, b, a)).containsExactly(a, b, c).inOrder();
+
+        // make A preferred over B
+        a = ScheduledRecording.buildFrom(a).setPriority(b.getPriority() + 2).build();
+        assertThat(sortByPriority(a, b, c)).containsExactly(b, c, a).inOrder();
+    }
+
+    public Collection<ScheduledRecording> sortByPriority(
+            ScheduledRecording a, ScheduledRecording b, ScheduledRecording c) {
+        List<ScheduledRecording> list = Arrays.asList(a, b, c);
+        Collections.sort(list, ScheduledRecording.PRIORITY_COMPARATOR);
+        return list;
+    }
+
+    private void assertOverLapping(boolean expected, long lower, long upper, ScheduledRecording r) {
+        assertWithMessage("isOverlapping(Range(" + lower + "," + upper + "), recording " + r)
+                .that(r.isOverLapping(new Range<>(lower, upper)))
+                .isEqualTo(expected);
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/dvr/data/SeriesRecordingTest.java b/tests/robotests/src/com/android/tv/dvr/data/SeriesRecordingTest.java
new file mode 100644
index 0000000..f1cc148
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/dvr/data/SeriesRecordingTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.dvr.data;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.os.Parcel;
+
+import com.android.tv.data.ProgramImpl;
+import com.android.tv.data.api.Program;
+import com.android.tv.testing.constants.ConfigConstants;
+
+import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link SeriesRecording}. */
+@RunWith(GoogleRobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class SeriesRecordingTest {
+    private static final String PROGRAM_TITLE = "MyProgram";
+    private static final long CHANNEL_ID = 123;
+    private static final long OTHER_CHANNEL_ID = 321;
+    private static final String SERIES_ID = "SERIES_ID";
+    private static final String OTHER_SERIES_ID = "OTHER_SERIES_ID";
+
+    private final SeriesRecording mBaseSeriesRecording =
+            new SeriesRecording.Builder()
+                    .setTitle(PROGRAM_TITLE)
+                    .setChannelId(CHANNEL_ID)
+                    .setSeriesId(SERIES_ID)
+                    .build();
+    private final SeriesRecording mSeriesRecordingSeason2 =
+            SeriesRecording.buildFrom(mBaseSeriesRecording).setStartFromSeason(2).build();
+    private final SeriesRecording mSeriesRecordingSeason2Episode5 =
+            SeriesRecording.buildFrom(mSeriesRecordingSeason2).setStartFromEpisode(5).build();
+    private final ProgramImpl mBaseProgram =
+            new ProgramImpl.Builder()
+                    .setTitle(PROGRAM_TITLE)
+                    .setChannelId(CHANNEL_ID)
+                    .setSeriesId(SERIES_ID)
+                    .build();
+
+    @Test
+    public void testParcelable() {
+        SeriesRecording r1 =
+                new SeriesRecording.Builder()
+                        .setId(1)
+                        .setChannelId(2)
+                        .setPriority(3)
+                        .setTitle("4")
+                        .setDescription("5")
+                        .setLongDescription("5-long")
+                        .setSeriesId("6")
+                        .setStartFromEpisode(7)
+                        .setStartFromSeason(8)
+                        .setChannelOption(SeriesRecording.OPTION_CHANNEL_ALL)
+                        .setCanonicalGenreIds(new int[] {9, 10})
+                        .setPosterUri("11")
+                        .setPhotoUri("12")
+                        .build();
+        Parcel p1 = Parcel.obtain();
+        Parcel p2 = Parcel.obtain();
+        try {
+            r1.writeToParcel(p1, 0);
+            byte[] bytes = p1.marshall();
+            p2.unmarshall(bytes, 0, bytes.length);
+            p2.setDataPosition(0);
+            SeriesRecording r2 = SeriesRecording.fromParcel(p2);
+            assertThat(r2).isEqualTo(r1);
+        } finally {
+            p1.recycle();
+            p2.recycle();
+        }
+    }
+
+    @Test
+    public void testDoesProgramMatch_simpleMatch() {
+        assertDoesProgramMatch(mBaseProgram, mBaseSeriesRecording, true);
+    }
+
+    @Test
+    public void testDoesProgramMatch_differentSeriesId() {
+        Program program =
+                new ProgramImpl.Builder(mBaseProgram).setSeriesId(OTHER_SERIES_ID).build();
+        assertDoesProgramMatch(program, mBaseSeriesRecording, false);
+    }
+
+    @Test
+    public void testDoesProgramMatch_differentChannel() {
+        Program program =
+                new ProgramImpl.Builder(mBaseProgram).setChannelId(OTHER_CHANNEL_ID).build();
+        assertDoesProgramMatch(program, mBaseSeriesRecording, false);
+    }
+
+    @Test
+    public void testDoesProgramMatch_startFromSeason2() {
+        ProgramImpl program = mBaseProgram;
+        assertDoesProgramMatch(program, mSeriesRecordingSeason2, true);
+        program = new ProgramImpl.Builder(program).setSeasonNumber("1").build();
+        assertDoesProgramMatch(program, mSeriesRecordingSeason2, false);
+        program = new ProgramImpl.Builder(program).setSeasonNumber("2").build();
+        assertDoesProgramMatch(program, mSeriesRecordingSeason2, true);
+        program = new ProgramImpl.Builder(program).setSeasonNumber("3").build();
+        assertDoesProgramMatch(program, mSeriesRecordingSeason2, true);
+    }
+
+    @Test
+    public void testDoesProgramMatch_startFromSeason2episode5() {
+        ProgramImpl program = mBaseProgram;
+        assertDoesProgramMatch(program, mSeriesRecordingSeason2Episode5, true);
+        program = new ProgramImpl.Builder(program).setSeasonNumber("2").build();
+        assertDoesProgramMatch(program, mSeriesRecordingSeason2Episode5, true);
+        program = new ProgramImpl.Builder(program).setEpisodeNumber("4").build();
+        assertDoesProgramMatch(program, mSeriesRecordingSeason2Episode5, false);
+        program = new ProgramImpl.Builder(program).setEpisodeNumber("5").build();
+        assertDoesProgramMatch(program, mSeriesRecordingSeason2Episode5, true);
+        program = new ProgramImpl.Builder(program).setEpisodeNumber("6").build();
+        assertDoesProgramMatch(program, mSeriesRecordingSeason2Episode5, true);
+        program =
+                new ProgramImpl.Builder(program).setSeasonNumber("3").setEpisodeNumber("1").build();
+        assertDoesProgramMatch(program, mSeriesRecordingSeason2Episode5, true);
+    }
+
+    private void assertDoesProgramMatch(
+            Program p, SeriesRecording seriesRecording, boolean expected) {
+        assertWithMessage(seriesRecording + " doesProgramMatch " + p)
+                .that(seriesRecording.matchProgram(p))
+                .isEqualTo(expected);
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/dvr/provider/DvrDbSyncTest.java b/tests/robotests/src/com/android/tv/dvr/provider/DvrDbSyncTest.java
new file mode 100644
index 0000000..92b4755
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/dvr/provider/DvrDbSyncTest.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.dvr.provider;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.ProgramImpl;
+import com.android.tv.data.api.Program;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.WritableDvrDataManager;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.recorder.SeriesRecordingScheduler;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.constants.ConfigConstants;
+
+import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.android.util.concurrent.RoboExecutorService;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link com.android.tv.dvr.DvrScheduleManager} */
+@RunWith(GoogleRobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK, application = TestSingletonApp.class)
+public class DvrDbSyncTest {
+    private static final String INPUT_ID = "input_id";
+    private static final long BASE_PROGRAM_ID = 1;
+    private static final long BASE_START_TIME_MS = 0;
+    private static final long BASE_END_TIME_MS = 1;
+    private static final String BASE_SEASON_NUMBER = "2";
+    private static final String BASE_EPISODE_NUMBER = "3";
+    private ProgramImpl baseProgram;
+    private ProgramImpl baseSeriesProgram;
+    private ScheduledRecording baseSchedule;
+    private ScheduledRecording baseSeriesSchedule;
+
+    private DvrDbSync mDbSync;
+    @Mock private DvrManager mDvrManager;
+    @Mock private WritableDvrDataManager mDataManager;
+    @Mock private ChannelDataManager mChannelDataManager;
+    @Mock private SeriesRecordingScheduler mSeriesRecordingScheduler;
+
+    @Before
+    public void setUp() {
+        // TODO(b/69843199): make these static finals
+        baseProgram =
+                new ProgramImpl.Builder()
+                        .setId(BASE_PROGRAM_ID)
+                        .setStartTimeUtcMillis(BASE_START_TIME_MS)
+                        .setEndTimeUtcMillis(BASE_END_TIME_MS)
+                        .build();
+        baseSeriesProgram =
+                new ProgramImpl.Builder()
+                        .setId(BASE_PROGRAM_ID)
+                        .setStartTimeUtcMillis(BASE_START_TIME_MS)
+                        .setEndTimeUtcMillis(BASE_END_TIME_MS)
+                        .setSeasonNumber(BASE_SEASON_NUMBER)
+                        .setEpisodeNumber(BASE_EPISODE_NUMBER)
+                        .build();
+        baseSchedule = ScheduledRecording.builder(INPUT_ID, baseProgram).build();
+        baseSeriesSchedule = ScheduledRecording.builder(INPUT_ID, baseSeriesProgram).build();
+
+        MockitoAnnotations.initMocks(this);
+        when(mChannelDataManager.isDbLoadFinished()).thenReturn(true);
+        when(mDvrManager.addSeriesRecording(any(), any(), anyInt()))
+                .thenReturn(SeriesRecording.builder(INPUT_ID, baseProgram).build());
+        mDbSync =
+                new DvrDbSync(
+                        RuntimeEnvironment.application.getApplicationContext(),
+                        mDataManager,
+                        mChannelDataManager,
+                        mDvrManager,
+                        mSeriesRecordingScheduler,
+                        new RoboExecutorService());
+    }
+
+    @Test
+    public void testHandleUpdateProgram_null() {
+        addSchedule(BASE_PROGRAM_ID, baseSchedule);
+        mDbSync.handleUpdateProgram(null, BASE_PROGRAM_ID);
+        verify(mDataManager).removeScheduledRecording(baseSchedule);
+    }
+
+    @Test
+    public void testHandleUpdateProgram_changeTimeNotStarted() {
+        addSchedule(BASE_PROGRAM_ID, baseSchedule);
+        long startTimeMs = BASE_START_TIME_MS + 1;
+        long endTimeMs = BASE_END_TIME_MS + 1;
+        Program program =
+                new ProgramImpl.Builder(baseProgram)
+                        .setStartTimeUtcMillis(startTimeMs)
+                        .setEndTimeUtcMillis(endTimeMs)
+                        .build();
+        mDbSync.handleUpdateProgram(program, BASE_PROGRAM_ID);
+        assertUpdateScheduleCalled(program);
+    }
+
+    @Test
+    public void testHandleUpdateProgram_changeTimeInProgressNotCalled() {
+        addSchedule(
+                BASE_PROGRAM_ID,
+                ScheduledRecording.buildFrom(baseSchedule)
+                        .setState(ScheduledRecording.STATE_RECORDING_IN_PROGRESS)
+                        .build());
+        long startTimeMs = BASE_START_TIME_MS + 1;
+        Program program =
+                new ProgramImpl.Builder(baseProgram).setStartTimeUtcMillis(startTimeMs).build();
+        mDbSync.handleUpdateProgram(program, BASE_PROGRAM_ID);
+        verify(mDataManager, never()).updateScheduledRecording(any());
+    }
+
+    @Test
+    public void testHandleUpdateProgram_changeSeason() {
+        addSchedule(BASE_PROGRAM_ID, baseSeriesSchedule);
+        String seasonNumber = BASE_SEASON_NUMBER + "1";
+        String episodeNumber = BASE_EPISODE_NUMBER + "1";
+        Program program =
+                new ProgramImpl.Builder(baseSeriesProgram)
+                        .setSeasonNumber(seasonNumber)
+                        .setEpisodeNumber(episodeNumber)
+                        .build();
+        mDbSync.handleUpdateProgram(program, BASE_PROGRAM_ID);
+        assertUpdateScheduleCalled(program);
+    }
+
+    @Test
+    public void testHandleUpdateProgram_finished() {
+        addSchedule(
+                BASE_PROGRAM_ID,
+                ScheduledRecording.buildFrom(baseSeriesSchedule)
+                        .setState(ScheduledRecording.STATE_RECORDING_FINISHED)
+                        .build());
+        String seasonNumber = BASE_SEASON_NUMBER + "1";
+        String episodeNumber = BASE_EPISODE_NUMBER + "1";
+        Program program =
+                new ProgramImpl.Builder(baseSeriesProgram)
+                        .setSeasonNumber(seasonNumber)
+                        .setEpisodeNumber(episodeNumber)
+                        .build();
+        mDbSync.handleUpdateProgram(program, BASE_PROGRAM_ID);
+        verify(mDataManager, never()).updateScheduledRecording(any());
+    }
+
+    private void addSchedule(long programId, ScheduledRecording schedule) {
+        when(mDataManager.getScheduledRecordingForProgramId(programId)).thenReturn(schedule);
+    }
+
+    private void assertUpdateScheduleCalled(Program program) {
+        verify(mDataManager)
+                .updateScheduledRecording(
+                        eq(ScheduledRecording.builder(INPUT_ID, program).build()));
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/dvr/provider/EpisodicProgramLoadTaskTest.java b/tests/robotests/src/com/android/tv/dvr/provider/EpisodicProgramLoadTaskTest.java
new file mode 100644
index 0000000..1244dc1
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/dvr/provider/EpisodicProgramLoadTaskTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.provider;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build.VERSION_CODES;
+import com.android.tv.dvr.data.SeasonEpisodeNumber;
+import com.android.tv.testing.TestSingletonApp;
+import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link EpisodicProgramLoadTask} */
+@RunWith(GoogleRobolectricTestRunner.class)
+@Config(
+    sdk = VERSION_CODES.N,
+    application = TestSingletonApp.class
+)
+public class EpisodicProgramLoadTaskTest {
+    private static final long SERIES_RECORDING_ID1 = 1;
+    private static final long SERIES_RECORDING_ID2 = 2;
+    private static final String SEASON_NUMBER1 = "SEASON NUMBER1";
+    private static final String SEASON_NUMBER2 = "SEASON NUMBER2";
+    private static final String EPISODE_NUMBER1 = "EPISODE NUMBER1";
+    private static final String EPISODE_NUMBER2 = "EPISODE NUMBER2";
+
+    @Test
+    public void testEpisodeAlreadyScheduled_true() {
+        List<SeasonEpisodeNumber> seasonEpisodeNumbers = new ArrayList<>();
+        SeasonEpisodeNumber seasonEpisodeNumber =
+                new SeasonEpisodeNumber(SERIES_RECORDING_ID1, SEASON_NUMBER1, EPISODE_NUMBER1);
+        seasonEpisodeNumbers.add(seasonEpisodeNumber);
+        assertThat(seasonEpisodeNumbers)
+                .contains(
+                        new SeasonEpisodeNumber(
+                                SERIES_RECORDING_ID1, SEASON_NUMBER1, EPISODE_NUMBER1));
+    }
+
+    @Test
+    public void testEpisodeAlreadyScheduled_false() {
+        List<SeasonEpisodeNumber> seasonEpisodeNumbers = new ArrayList<>();
+        SeasonEpisodeNumber seasonEpisodeNumber =
+                new SeasonEpisodeNumber(SERIES_RECORDING_ID1, SEASON_NUMBER1, EPISODE_NUMBER1);
+        seasonEpisodeNumbers.add(seasonEpisodeNumber);
+        assertThat(seasonEpisodeNumbers)
+                .doesNotContain(
+                        new SeasonEpisodeNumber(
+                                SERIES_RECORDING_ID2, SEASON_NUMBER1, EPISODE_NUMBER1));
+        assertThat(seasonEpisodeNumbers)
+                .doesNotContain(
+                        new SeasonEpisodeNumber(
+                                SERIES_RECORDING_ID1, SEASON_NUMBER2, EPISODE_NUMBER1));
+        assertThat(seasonEpisodeNumbers)
+                .doesNotContain(
+                        new SeasonEpisodeNumber(
+                                SERIES_RECORDING_ID1, SEASON_NUMBER1, EPISODE_NUMBER2));
+    }
+
+    @Test
+    public void testEpisodeAlreadyScheduled_null() {
+        List<SeasonEpisodeNumber> seasonEpisodeNumbers = new ArrayList<>();
+        SeasonEpisodeNumber seasonEpisodeNumber =
+                new SeasonEpisodeNumber(SERIES_RECORDING_ID1, SEASON_NUMBER1, EPISODE_NUMBER1);
+        seasonEpisodeNumbers.add(seasonEpisodeNumber);
+        assertThat(seasonEpisodeNumbers)
+                .doesNotContain(
+                        new SeasonEpisodeNumber(SERIES_RECORDING_ID1, null, EPISODE_NUMBER1));
+        assertThat(seasonEpisodeNumbers)
+                .doesNotContain(
+                        new SeasonEpisodeNumber(SERIES_RECORDING_ID1, SEASON_NUMBER1, null));
+        assertThat(seasonEpisodeNumbers)
+                .doesNotContain(new SeasonEpisodeNumber(SERIES_RECORDING_ID1, null, null));
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/dvr/recorder/InputTaskSchedulerTest.java b/tests/robotests/src/com/android/tv/dvr/recorder/InputTaskSchedulerTest.java
new file mode 100644
index 0000000..ade3ae8
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/dvr/recorder/InputTaskSchedulerTest.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.dvr.recorder;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AlarmManager;
+import android.media.tv.TvInputInfo;
+import android.os.Build;
+import android.os.Looper;
+import android.os.SystemClock;
+import com.android.tv.InputSessionManager;
+import com.android.tv.common.util.Clock;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.api.Channel;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.WritableDvrDataManager;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.recorder.InputTaskScheduler.RecordingTaskFactory;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.dvr.RecordingTestUtils;
+import com.android.tv.testing.fakes.FakeClock;
+import com.android.tv.testing.utils.TestUtils;
+import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link InputTaskScheduler}. */
+@RunWith(GoogleRobolectricTestRunner.class)
+@Config(sdk = Build.VERSION_CODES.N, application = TestSingletonApp.class)
+public class InputTaskSchedulerTest {
+    private static final String INPUT_ID = "input_id";
+    private static final int CHANNEL_ID = 1;
+    private static final long LISTENER_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(1);
+    private static final int TUNER_COUNT_ONE = 1;
+    private static final int TUNER_COUNT_TWO = 2;
+    private static final long LOW_PRIORITY = 1;
+    private static final long HIGH_PRIORITY = 2;
+
+    private FakeClock mFakeClock;
+    private InputTaskScheduler mScheduler;
+    @Mock private DvrManager mDvrManager;
+    @Mock private WritableDvrDataManager mDataManager;
+    @Mock private InputSessionManager mSessionManager;
+    @Mock private AlarmManager mMockAlarmManager;
+    @Mock private ChannelDataManager mChannelDataManager;
+    private List<RecordingTask> mRecordingTasks;
+
+    @Before
+    public void setUp() throws Exception {
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+        mRecordingTasks = new ArrayList();
+        MockitoAnnotations.initMocks(this);
+        mFakeClock = FakeClock.createWithCurrentTime();
+        TvInputInfo input = createTvInputInfo(TUNER_COUNT_ONE);
+        mScheduler =
+                new InputTaskScheduler(
+                        RuntimeEnvironment.application,
+                        input,
+                        Looper.myLooper(),
+                        mChannelDataManager,
+                        mDvrManager,
+                        mDataManager,
+                        mSessionManager,
+                        mFakeClock,
+                        new RecordingTaskFactory() {
+                            @Override
+                            public RecordingTask createRecordingTask(
+                                    ScheduledRecording scheduledRecording,
+                                    Channel channel,
+                                    DvrManager dvrManager,
+                                    InputSessionManager sessionManager,
+                                    WritableDvrDataManager dataManager,
+                                    Clock clock) {
+                                RecordingTask task = mock(RecordingTask.class);
+                                when(task.getPriority())
+                                        .thenReturn(scheduledRecording.getPriority());
+                                when(task.getEndTimeMs())
+                                        .thenReturn(scheduledRecording.getEndTimeMs());
+                                mRecordingTasks.add(task);
+                                return task;
+                            }
+                        });
+    }
+
+    @Test
+    public void testAddSchedule_past() {
+        ScheduledRecording r =
+                RecordingTestUtils.createTestRecordingWithPeriod(INPUT_ID, CHANNEL_ID, 0L, 1L);
+        when(mDataManager.getScheduledRecording(anyLong())).thenReturn(r);
+        mScheduler.handleAddSchedule(r);
+        mScheduler.handleBuildSchedule();
+        verify(mDataManager, timeout((int) LISTENER_TIMEOUT_MS).times(1))
+                .changeState(
+                        any(ScheduledRecording.class),
+                        eq(ScheduledRecording.STATE_RECORDING_FAILED),
+                        eq(ScheduledRecording
+                                .FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED));
+    }
+
+    @Test
+    public void testAddSchedule_start() {
+        mScheduler.handleAddSchedule(
+                RecordingTestUtils.createTestRecordingWithPeriod(
+                        INPUT_ID,
+                        CHANNEL_ID,
+                        mFakeClock.currentTimeMillis(),
+                        mFakeClock.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)));
+        mScheduler.handleBuildSchedule();
+        verify(mRecordingTasks.get(0), timeout((int) LISTENER_TIMEOUT_MS).times(1)).start();
+    }
+
+    @Test
+    public void testAddSchedule_consecutiveNoStop() {
+        long startTimeMs = mFakeClock.currentTimeMillis();
+        long endTimeMs = startTimeMs + TimeUnit.SECONDS.toMillis(1);
+        long id = 0;
+        mScheduler.handleAddSchedule(
+                RecordingTestUtils.createTestRecordingWithIdAndPriorityAndPeriod(
+                        ++id, CHANNEL_ID, LOW_PRIORITY, startTimeMs, endTimeMs));
+        mScheduler.handleBuildSchedule();
+        startTimeMs = endTimeMs;
+        endTimeMs = startTimeMs + TimeUnit.SECONDS.toMillis(1);
+        mScheduler.handleAddSchedule(
+                RecordingTestUtils.createTestRecordingWithIdAndPriorityAndPeriod(
+                        ++id, CHANNEL_ID, HIGH_PRIORITY, startTimeMs, endTimeMs));
+        mScheduler.handleBuildSchedule();
+        verify(mRecordingTasks.get(0), timeout((int) LISTENER_TIMEOUT_MS).times(1)).start();
+        // The first schedule should not be stopped because the second one should wait for the end
+        // of the first schedule.
+        SystemClock.sleep(LISTENER_TIMEOUT_MS);
+        verify(mRecordingTasks.get(0), never()).stop();
+    }
+
+    @Test
+    public void testAddSchedule_consecutiveNoFail() {
+        long startTimeMs = mFakeClock.currentTimeMillis();
+        long endTimeMs = startTimeMs + TimeUnit.SECONDS.toMillis(1);
+        long id = 0;
+        when(mDataManager.getScheduledRecording(anyLong()))
+                .thenReturn(ScheduledRecording.builder(INPUT_ID, CHANNEL_ID, 0L, 0L).build());
+        mScheduler.handleAddSchedule(
+                RecordingTestUtils.createTestRecordingWithIdAndPriorityAndPeriod(
+                        ++id, CHANNEL_ID, HIGH_PRIORITY, startTimeMs, endTimeMs));
+        mScheduler.handleBuildSchedule();
+        startTimeMs = endTimeMs;
+        endTimeMs = startTimeMs + TimeUnit.SECONDS.toMillis(1);
+        mScheduler.handleAddSchedule(
+                RecordingTestUtils.createTestRecordingWithIdAndPriorityAndPeriod(
+                        ++id, CHANNEL_ID, LOW_PRIORITY, startTimeMs, endTimeMs));
+        mScheduler.handleBuildSchedule();
+        verify(mRecordingTasks.get(0), timeout((int) LISTENER_TIMEOUT_MS).times(1)).start();
+        SystemClock.sleep(LISTENER_TIMEOUT_MS);
+        verify(mRecordingTasks.get(0), never()).stop();
+        // The second schedule should not fail because it can starts after the first one finishes.
+        SystemClock.sleep(LISTENER_TIMEOUT_MS);
+        verify(mDataManager, never())
+                .changeState(
+                        any(ScheduledRecording.class),
+                        eq(ScheduledRecording.STATE_RECORDING_FAILED));
+    }
+
+    @Test
+    public void testAddSchedule_consecutiveUseLessSession() throws Exception {
+        TvInputInfo input = createTvInputInfo(TUNER_COUNT_TWO);
+        mScheduler.updateTvInputInfo(input);
+        long startTimeMs = mFakeClock.currentTimeMillis();
+        long endTimeMs = startTimeMs + TimeUnit.SECONDS.toMillis(1);
+        long id = 0;
+        mScheduler.handleAddSchedule(
+                RecordingTestUtils.createTestRecordingWithIdAndPriorityAndPeriod(
+                        ++id, CHANNEL_ID, LOW_PRIORITY, startTimeMs, endTimeMs));
+        mScheduler.handleBuildSchedule();
+        startTimeMs = endTimeMs;
+        endTimeMs = startTimeMs + TimeUnit.SECONDS.toMillis(1);
+        mScheduler.handleAddSchedule(
+                RecordingTestUtils.createTestRecordingWithIdAndPriorityAndPeriod(
+                        ++id, CHANNEL_ID, HIGH_PRIORITY, startTimeMs, endTimeMs));
+        mScheduler.handleBuildSchedule();
+        verify(mRecordingTasks.get(0), timeout((int) LISTENER_TIMEOUT_MS).times(1)).start();
+        SystemClock.sleep(LISTENER_TIMEOUT_MS);
+        verify(mRecordingTasks.get(0), never()).stop();
+        // The second schedule should wait until the first one finishes rather than creating a new
+        // session even though there are available tuners.
+        assertTrue(mRecordingTasks.size() == 1);
+    }
+
+    @Test
+    public void testUpdateSchedule_noCancel() {
+        ScheduledRecording r =
+                RecordingTestUtils.createTestRecordingWithPeriod(
+                        INPUT_ID,
+                        CHANNEL_ID,
+                        mFakeClock.currentTimeMillis(),
+                        mFakeClock.currentTimeMillis() + TimeUnit.HOURS.toMillis(1));
+        mScheduler.handleAddSchedule(r);
+        mScheduler.handleBuildSchedule();
+        mScheduler.handleUpdateSchedule(r);
+        SystemClock.sleep(LISTENER_TIMEOUT_MS);
+        verify(mRecordingTasks.get(0), never()).cancel();
+    }
+
+    @Test
+    public void testUpdateSchedule_cancel() {
+        ScheduledRecording r =
+                RecordingTestUtils.createTestRecordingWithPeriod(
+                        INPUT_ID,
+                        CHANNEL_ID,
+                        mFakeClock.currentTimeMillis(),
+                        mFakeClock.currentTimeMillis() + TimeUnit.HOURS.toMillis(2));
+        mScheduler.handleAddSchedule(r);
+        mScheduler.handleBuildSchedule();
+        mScheduler.handleUpdateSchedule(
+                ScheduledRecording.buildFrom(r)
+                        .setStartTimeMs(mFakeClock.currentTimeMillis() + TimeUnit.HOURS.toMillis(1))
+                        .build());
+        verify(mRecordingTasks.get(0), timeout((int) LISTENER_TIMEOUT_MS).times(1)).cancel();
+    }
+
+    private TvInputInfo createTvInputInfo(int tunerCount) throws Exception {
+        return TestUtils.createTvInputInfo(null, null, null, 0, false, true, tunerCount);
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/dvr/recorder/RecordingTaskTest.java b/tests/robotests/src/com/android/tv/dvr/recorder/RecordingTaskTest.java
new file mode 100644
index 0000000..7a62563
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/dvr/recorder/RecordingTaskTest.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.dvr.recorder;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import com.android.tv.InputSessionManager;
+import com.android.tv.InputSessionManager.RecordingSession;
+import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.feature.TestableFeature;
+import com.android.tv.data.ChannelImpl;
+import com.android.tv.data.api.Channel;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.recorder.RecordingTask.State;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.dvr.DvrDataManagerInMemoryImpl;
+import com.android.tv.testing.dvr.RecordingTestUtils;
+import com.android.tv.testing.fakes.FakeClock;
+import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link RecordingTask}. */
+@RunWith(GoogleRobolectricTestRunner.class)
+@Config(sdk = Build.VERSION_CODES.N, application = TestSingletonApp.class)
+public class RecordingTaskTest {
+    private static final long DURATION = TimeUnit.MINUTES.toMillis(30);
+    private static final long START_OFFSET_MS = RecordingScheduler.MS_TO_WAKE_BEFORE_START;
+    private static final String INPUT_ID = "input_id";
+    private static final int CHANNEL_ID = 273;
+
+    private FakeClock mFakeClock;
+    private DvrDataManagerInMemoryImpl mDataManager;
+    @Mock Handler mMockHandler;
+    @Mock DvrManager mDvrManager;
+    @Mock InputSessionManager mMockSessionManager;
+    @Mock RecordingSession mMockRecordingSession;
+    private final TestableFeature mDvrFeature = CommonFeatures.DVR;
+
+    @Before
+    public void setUp() {
+        mDvrFeature.enableForTest();
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+        MockitoAnnotations.initMocks(this);
+        mFakeClock = FakeClock.createWithCurrentTime();
+        mDataManager = new DvrDataManagerInMemoryImpl(RuntimeEnvironment.application, mFakeClock);
+    }
+
+    @After
+    public void tearDown() {
+        mDvrFeature.resetForTests();
+    }
+
+    @Test
+    public void testHandle_init() {
+        Channel channel = createTestChannel();
+        ScheduledRecording r = createRecording(channel);
+        RecordingTask task = createRecordingTask(r, channel);
+        String inputId = channel.getInputId();
+        when(mMockSessionManager.createRecordingSession(
+                        eq(inputId), anyString(), eq(task), eq(mMockHandler), anyLong()))
+                .thenReturn(mMockRecordingSession);
+    when(mMockHandler.sendMessageAtTime(any(), anyLong())).thenReturn(true);
+        assertTrue(task.handleMessage(createMessage(RecordingTask.MSG_INITIALIZE)));
+        assertEquals(State.CONNECTION_PENDING, task.getState());
+        verify(mMockSessionManager)
+                .createRecordingSession(
+                        eq(inputId), anyString(), eq(task), eq(mMockHandler), anyLong());
+        verify(mMockRecordingSession).tune(eq(inputId), eq(channel.getUri()));
+        verifyNoMoreInteractions(mMockHandler, mMockRecordingSession, mMockSessionManager);
+    }
+
+    private static Channel createTestChannel() {
+        return new ChannelImpl.Builder()
+                .setInputId(INPUT_ID)
+                .setId(CHANNEL_ID)
+                .setDisplayName("Test Ch " + CHANNEL_ID)
+                .build();
+    }
+
+    @Test
+    public void testOnConnected() {
+        Channel channel = createTestChannel();
+        ScheduledRecording r = createRecording(channel);
+        mDataManager.addScheduledRecording(r);
+        RecordingTask task = createRecordingTask(r, channel);
+        String inputId = channel.getInputId();
+        when(mMockSessionManager.createRecordingSession(
+                        eq(inputId), anyString(), eq(task), eq(mMockHandler), anyLong()))
+                .thenReturn(mMockRecordingSession);
+        when(mMockHandler.sendEmptyMessageDelayed(anyInt(), anyLong())).thenReturn(true);
+        task.handleMessage(createMessage(RecordingTask.MSG_INITIALIZE));
+        task.onTuned(channel.getUri());
+        assertEquals(State.CONNECTED, task.getState());
+    }
+
+    private ScheduledRecording createRecording(Channel c) {
+        long startTime = mFakeClock.currentTimeMillis() + START_OFFSET_MS;
+        long endTime = startTime + DURATION;
+        return RecordingTestUtils.createTestRecordingWithPeriod(
+                c.getInputId(), c.getId(), startTime, endTime);
+    }
+
+    private RecordingTask createRecordingTask(ScheduledRecording r, Channel channel) {
+        RecordingTask recordingTask =
+                new RecordingTask(
+                        RuntimeEnvironment.application,
+                        r,
+                        channel,
+                        mDvrManager,
+                        mMockSessionManager,
+                        mDataManager,
+                        mFakeClock);
+        recordingTask.setHandler(mMockHandler);
+        return recordingTask;
+    }
+
+    private Message createMessage(int what) {
+        Message msg = new Message();
+        msg.setTarget(mMockHandler);
+        msg.what = what;
+        return msg;
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/dvr/recorder/ScheduledProgramReaperTest.java b/tests/robotests/src/com/android/tv/dvr/recorder/ScheduledProgramReaperTest.java
new file mode 100644
index 0000000..9381de2
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/dvr/recorder/ScheduledProgramReaperTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.recorder;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Build;
+import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.feature.TestableFeature;
+import com.android.tv.common.util.CommonUtils;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.dvr.DvrDataManagerInMemoryImpl;
+import com.android.tv.testing.dvr.RecordingTestUtils;
+import com.android.tv.testing.fakes.FakeClock;
+import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link ScheduledProgramReaper}. */
+@RunWith(GoogleRobolectricTestRunner.class)
+@Config(sdk = Build.VERSION_CODES.N, application = TestSingletonApp.class)
+public class ScheduledProgramReaperTest {
+    private static final String INPUT_ID = "input_id";
+    private static final int CHANNEL_ID = 273;
+    private static final long DURATION = TimeUnit.HOURS.toMillis(1);
+
+    private ScheduledProgramReaper mReaper;
+    private FakeClock mFakeClock;
+    private DvrDataManagerInMemoryImpl mDvrDataManager;
+    @Mock private DvrManager mDvrManager;
+    private final TestableFeature mDvrFeature = CommonFeatures.DVR;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mDvrFeature.enableForTest();
+        mFakeClock = FakeClock.createWithTimeOne();
+        mDvrDataManager =
+                new DvrDataManagerInMemoryImpl(RuntimeEnvironment.application, mFakeClock);
+        mReaper = new ScheduledProgramReaper(mDvrDataManager, mFakeClock);
+    }
+
+    @After
+    public void tearDown() {
+        mDvrFeature.resetForTests();
+    }
+
+    @Test
+    public void testRun_noRecordings() {
+        assertTrue(mDvrDataManager.getAllScheduledRecordings().isEmpty());
+        mReaper.run();
+        assertTrue(mDvrDataManager.getAllScheduledRecordings().isEmpty());
+    }
+
+    @Test
+    public void testRun_oneRecordingsTomorrow() {
+        ScheduledRecording recording = addNewScheduledRecordingForTomorrow();
+        assertThat(mDvrDataManager.getAllScheduledRecordings()).containsExactly(recording);
+        mReaper.run();
+        assertThat(mDvrDataManager.getAllScheduledRecordings()).containsExactly(recording);
+    }
+
+    @Test
+    public void testRun_oneRecordingsStarted() {
+        ScheduledRecording recording = addNewScheduledRecordingForTomorrow();
+        assertThat(mDvrDataManager.getAllScheduledRecordings()).containsExactly(recording);
+        mFakeClock.increment(TimeUnit.DAYS);
+        mReaper.run();
+        assertThat(mDvrDataManager.getAllScheduledRecordings()).containsExactly(recording);
+    }
+
+    @Test
+    public void testRun_oneRecordingsFinished() {
+        ScheduledRecording recording = addNewScheduledRecordingForTomorrow();
+        assertThat(mDvrDataManager.getAllScheduledRecordings()).containsExactly(recording);
+        mFakeClock.increment(TimeUnit.DAYS);
+        mFakeClock.increment(TimeUnit.MINUTES, 2);
+        mReaper.run();
+        assertThat(mDvrDataManager.getAllScheduledRecordings()).containsExactly(recording);
+    }
+
+    @Test
+    public void testRun_oneRecordingsExpired() {
+        ScheduledRecording recording = addNewScheduledRecordingForTomorrow();
+        assertThat(mDvrDataManager.getAllScheduledRecordings()).containsExactly(recording);
+        mFakeClock.increment(TimeUnit.DAYS, 1 + ScheduledProgramReaper.DAYS);
+        mFakeClock.increment(TimeUnit.MILLISECONDS, DURATION);
+        // After the cutoff and enough so we can see on the clock
+        mFakeClock.increment(TimeUnit.SECONDS, 1);
+
+        mReaper.run();
+        assertTrue(
+                "Recordings after reaper at "
+                        + CommonUtils.toIsoDateTimeString(mFakeClock.currentTimeMillis()),
+                mDvrDataManager.getAllScheduledRecordings().isEmpty());
+    }
+
+    private ScheduledRecording addNewScheduledRecordingForTomorrow() {
+        long startTime = mFakeClock.currentTimeMillis() + TimeUnit.DAYS.toMillis(1);
+        ScheduledRecording recording =
+                RecordingTestUtils.createTestRecordingWithPeriod(
+                        INPUT_ID, CHANNEL_ID, startTime, startTime + DURATION);
+        return mDvrDataManager.addScheduledRecordingInternal(
+                ScheduledRecording.buildFrom(recording)
+                        .setState(ScheduledRecording.STATE_RECORDING_FINISHED)
+                        .build());
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/dvr/recorder/SchedulerTest.java b/tests/robotests/src/com/android/tv/dvr/recorder/SchedulerTest.java
new file mode 100644
index 0000000..3060c47
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/dvr/recorder/SchedulerTest.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.dvr.recorder;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.os.Build;
+import android.os.Looper;
+import com.android.tv.InputSessionManager;
+import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.feature.TestableFeature;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.dvr.DvrDataManagerInMemoryImpl;
+import com.android.tv.testing.dvr.RecordingTestUtils;
+import com.android.tv.testing.fakes.FakeClock;
+import com.android.tv.util.TvInputManagerHelper;
+import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link RecordingScheduler}. */
+@RunWith(GoogleRobolectricTestRunner.class)
+@Config(sdk = Build.VERSION_CODES.N, application = TestSingletonApp.class)
+public class SchedulerTest {
+    private static final String INPUT_ID = "input_id";
+    private static final int CHANNEL_ID = 273;
+
+    private FakeClock mFakeClock;
+    private DvrDataManagerInMemoryImpl mDataManager;
+    private RecordingScheduler mScheduler;
+    @Mock DvrManager mDvrManager;
+    @Mock InputSessionManager mSessionManager;
+    @Mock AlarmManager mMockAlarmManager;
+    @Mock ChannelDataManager mChannelDataManager;
+    @Mock TvInputManagerHelper mInputManager;
+    private final TestableFeature mDvrFeature = CommonFeatures.DVR;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mDvrFeature.enableForTest();
+        mFakeClock = FakeClock.createWithCurrentTime();
+        mDataManager = new DvrDataManagerInMemoryImpl(RuntimeEnvironment.application, mFakeClock);
+        Mockito.when(mChannelDataManager.isDbLoadFinished()).thenReturn(true);
+        mScheduler =
+                new RecordingScheduler(
+                        Looper.myLooper(),
+                        mDvrManager,
+                        mSessionManager,
+                        mDataManager,
+                        mChannelDataManager,
+                        mInputManager,
+                        RuntimeEnvironment.application,
+                        mFakeClock,
+                        mMockAlarmManager);
+    }
+
+    @After
+    public void tearDown() {
+        mDvrFeature.resetForTests();
+    }
+
+    @Test
+    public void testUpdate_none() {
+        mScheduler.updateAndStartServiceIfNeeded();
+        verifyZeroInteractions(mMockAlarmManager);
+    }
+
+    @Test
+    public void testUpdate_nextIn12Hours() {
+        long now = mFakeClock.currentTimeMillis();
+        long startTime = now + TimeUnit.HOURS.toMillis(12);
+        ScheduledRecording r =
+                RecordingTestUtils.createTestRecordingWithPeriod(
+                        INPUT_ID, CHANNEL_ID, startTime, startTime + TimeUnit.HOURS.toMillis(1));
+        mDataManager.addScheduledRecording(r);
+        verify(mMockAlarmManager)
+                .setExactAndAllowWhileIdle(
+                        eq(AlarmManager.RTC_WAKEUP),
+                        eq(startTime - RecordingScheduler.MS_TO_WAKE_BEFORE_START),
+                        any(PendingIntent.class));
+        Mockito.reset(mMockAlarmManager);
+        mScheduler.updateAndStartServiceIfNeeded();
+        verify(mMockAlarmManager)
+                .setExactAndAllowWhileIdle(
+                        eq(AlarmManager.RTC_WAKEUP),
+                        eq(startTime - RecordingScheduler.MS_TO_WAKE_BEFORE_START),
+                        any(PendingIntent.class));
+    }
+
+    @Test
+    public void testStartsWithin() {
+        long now = mFakeClock.currentTimeMillis();
+        long startTime = now + 3;
+        ScheduledRecording r =
+                RecordingTestUtils.createTestRecordingWithPeriod(
+                        INPUT_ID, CHANNEL_ID, startTime, startTime + 100);
+        assertFalse(mScheduler.startsWithin(r, 2));
+        assertTrue(mScheduler.startsWithin(r, 3));
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/dvr/recorder/SeriesRecordingSchedulerTest.java b/tests/robotests/src/com/android/tv/dvr/recorder/SeriesRecordingSchedulerTest.java
new file mode 100644
index 0000000..f8d6519
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/dvr/recorder/SeriesRecordingSchedulerTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.recorder;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+import android.util.LongSparseArray;
+
+import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.feature.TestableFeature;
+import com.android.tv.data.ProgramImpl;
+import com.android.tv.data.api.Program;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.dvr.DvrDataManagerInMemoryImpl;
+import com.android.tv.testing.fakes.FakeClock;
+
+import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Tests for {@link SeriesRecordingScheduler} */
+@RunWith(GoogleRobolectricTestRunner.class)
+@Config(sdk = Build.VERSION_CODES.N, application = TestSingletonApp.class)
+public class SeriesRecordingSchedulerTest {
+    private static final String PROGRAM_TITLE = "MyProgram";
+    private static final long CHANNEL_ID = 123;
+    private static final long SERIES_RECORDING_ID1 = 1;
+    private static final String SERIES_ID = "SERIES_ID";
+    private static final String SEASON_NUMBER1 = "SEASON NUMBER1";
+    private static final String SEASON_NUMBER2 = "SEASON NUMBER2";
+    private static final String EPISODE_NUMBER1 = "EPISODE NUMBER1";
+    private static final String EPISODE_NUMBER2 = "EPISODE NUMBER2";
+
+    private final SeriesRecording mBaseSeriesRecording =
+            new SeriesRecording.Builder()
+                    .setTitle(PROGRAM_TITLE)
+                    .setChannelId(CHANNEL_ID)
+                    .setSeriesId(SERIES_ID)
+                    .build();
+    private final ProgramImpl mBaseProgram =
+            new ProgramImpl.Builder()
+                    .setTitle(PROGRAM_TITLE)
+                    .setChannelId(CHANNEL_ID)
+                    .setSeriesId(SERIES_ID)
+                    .build();
+    private final TestableFeature mDvrFeature = CommonFeatures.DVR;
+
+    private DvrDataManagerInMemoryImpl mDataManager;
+
+    @Before
+    public void setUp() {
+        mDvrFeature.enableForTest();
+        FakeClock fakeClock = FakeClock.createWithCurrentTime();
+        mDataManager = new DvrDataManagerInMemoryImpl(RuntimeEnvironment.application, fakeClock);
+    }
+
+    @After
+    public void tearDown() {
+        mDvrFeature.resetForTests();
+    }
+
+    @Test
+    public void testPickOneProgramPerEpisode_onePerEpisode() {
+        SeriesRecording seriesRecording =
+                SeriesRecording.buildFrom(mBaseSeriesRecording).setId(SERIES_RECORDING_ID1).build();
+        mDataManager.addSeriesRecording(seriesRecording);
+        List<Program> programs = new ArrayList<>();
+        Program program1 =
+                new ProgramImpl.Builder(mBaseProgram)
+                        .setSeasonNumber(SEASON_NUMBER1)
+                        .setEpisodeNumber(EPISODE_NUMBER1)
+                        .build();
+        programs.add(program1);
+        Program program2 =
+                new ProgramImpl.Builder(mBaseProgram)
+                        .setSeasonNumber(SEASON_NUMBER2)
+                        .setEpisodeNumber(EPISODE_NUMBER2)
+                        .build();
+        programs.add(program2);
+        LongSparseArray<List<Program>> result =
+                SeriesRecordingScheduler.pickOneProgramPerEpisode(
+                        mDataManager, Collections.singletonList(seriesRecording), programs);
+        assertThat(result.get(SERIES_RECORDING_ID1)).containsExactly(program1, program2);
+    }
+
+    @Test
+    public void testPickOneProgramPerEpisode_manyPerEpisode() {
+        SeriesRecording seriesRecording =
+                SeriesRecording.buildFrom(mBaseSeriesRecording).setId(SERIES_RECORDING_ID1).build();
+        mDataManager.addSeriesRecording(seriesRecording);
+        List<Program> programs = new ArrayList<>();
+        ProgramImpl program1 =
+                new ProgramImpl.Builder(mBaseProgram)
+                        .setSeasonNumber(SEASON_NUMBER1)
+                        .setEpisodeNumber(EPISODE_NUMBER1)
+                        .setStartTimeUtcMillis(0)
+                        .build();
+        programs.add(program1);
+        Program program2 = new ProgramImpl.Builder(program1).setStartTimeUtcMillis(1).build();
+        programs.add(program2);
+        Program program3 =
+                new ProgramImpl.Builder(mBaseProgram)
+                        .setSeasonNumber(SEASON_NUMBER2)
+                        .setEpisodeNumber(EPISODE_NUMBER2)
+                        .build();
+        programs.add(program3);
+        Program program4 = new ProgramImpl.Builder(program1).setStartTimeUtcMillis(1).build();
+        programs.add(program4);
+        LongSparseArray<List<Program>> result =
+                SeriesRecordingScheduler.pickOneProgramPerEpisode(
+                        mDataManager, Collections.singletonList(seriesRecording), programs);
+        assertThat(result.get(SERIES_RECORDING_ID1)).containsExactly(program1, program3);
+    }
+
+    @Test
+    public void testPickOneProgramPerEpisode_nullEpisode() {
+        SeriesRecording seriesRecording =
+                SeriesRecording.buildFrom(mBaseSeriesRecording).setId(SERIES_RECORDING_ID1).build();
+        mDataManager.addSeriesRecording(seriesRecording);
+        List<Program> programs = new ArrayList<>();
+        Program program1 = new ProgramImpl.Builder(mBaseProgram).setStartTimeUtcMillis(0).build();
+        programs.add(program1);
+        Program program2 = new ProgramImpl.Builder(mBaseProgram).setStartTimeUtcMillis(1).build();
+        programs.add(program2);
+        LongSparseArray<List<Program>> result =
+                SeriesRecordingScheduler.pickOneProgramPerEpisode(
+                        mDataManager, Collections.singletonList(seriesRecording), programs);
+        assertThat(result.get(SERIES_RECORDING_ID1)).containsExactly(program1, program2);
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/dvr/ui/SortedArrayAdapterTest.java b/tests/robotests/src/com/android/tv/dvr/ui/SortedArrayAdapterTest.java
new file mode 100644
index 0000000..94ce184
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/dvr/ui/SortedArrayAdapterTest.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import androidx.leanback.widget.ClassPresenterSelector;
+import androidx.leanback.widget.ObjectAdapter;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.google.thirdparty.robolectric.GoogleRobolectricTestRunner;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Objects;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link SortedArrayAdapter}. */
+@RunWith(GoogleRobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class SortedArrayAdapterTest {
+    public static final TestData P1 = TestData.create(1, "c");
+    public static final TestData P2 = TestData.create(2, "b");
+    public static final TestData P3 = TestData.create(3, "a");
+    public static final TestData EXTRA = TestData.create(4, "k");
+    private TestSortedArrayAdapter mAdapter;
+
+    @Before
+    public void setUp() {
+        mAdapter = new TestSortedArrayAdapter(Integer.MAX_VALUE, null);
+    }
+
+    @Test
+    public void testContents_empty() {
+        assertEmpty();
+    }
+
+    @Test
+    public void testAdd_one() {
+        mAdapter.add(P1);
+        assertNotEmpty();
+        assertContentsInOrder(mAdapter, P1);
+    }
+
+    @Test
+    public void testAdd_two() {
+        mAdapter.add(P1);
+        mAdapter.add(P2);
+        assertNotEmpty();
+        assertContentsInOrder(mAdapter, P2, P1);
+    }
+
+    @Test
+    public void testSetInitialItems_two() {
+        mAdapter.setInitialItems(Arrays.asList(P1, P2));
+        assertNotEmpty();
+        assertContentsInOrder(mAdapter, P2, P1);
+    }
+
+    @Test
+    public void testMaxInitialCount() {
+        mAdapter = new TestSortedArrayAdapter(1, null);
+        mAdapter.setInitialItems(Arrays.asList(P1, P2));
+        assertNotEmpty();
+        assertThat(mAdapter.size()).isEqualTo(1);
+        assertThat(mAdapter.get(0)).isEqualTo(P2);
+    }
+
+    @Test
+    public void testExtraItem() {
+        mAdapter = new TestSortedArrayAdapter(Integer.MAX_VALUE, EXTRA);
+        mAdapter.setInitialItems(Arrays.asList(P1, P2));
+        assertNotEmpty();
+        assertThat(mAdapter.size()).isEqualTo(3);
+        assertThat(mAdapter.get(0)).isEqualTo(P2);
+        assertThat(mAdapter.get(2)).isEqualTo(EXTRA);
+        mAdapter.remove(P2);
+        mAdapter.remove(P1);
+        assertThat(mAdapter.size()).isEqualTo(1);
+        assertThat(mAdapter.get(0)).isEqualTo(EXTRA);
+    }
+
+    @Test
+    public void testExtraItemWithMaxCount() {
+        mAdapter = new TestSortedArrayAdapter(1, EXTRA);
+        mAdapter.setInitialItems(Arrays.asList(P1, P2));
+        assertNotEmpty();
+        assertThat(mAdapter.size()).isEqualTo(2);
+        assertThat(mAdapter.get(0)).isEqualTo(P2);
+        assertThat(mAdapter.get(1)).isEqualTo(EXTRA);
+        mAdapter.remove(P2);
+        assertThat(mAdapter.size()).isEqualTo(1);
+        assertThat(mAdapter.get(0)).isEqualTo(EXTRA);
+    }
+
+    @Test
+    public void testRemove() {
+        mAdapter.add(P1);
+        mAdapter.add(P2);
+        assertNotEmpty();
+        assertContentsInOrder(mAdapter, P2, P1);
+        mAdapter.remove(P3);
+        assertContentsInOrder(mAdapter, P2, P1);
+        mAdapter.remove(P2);
+        assertContentsInOrder(mAdapter, P1);
+        mAdapter.remove(P1);
+        assertEmpty();
+        mAdapter.add(P1);
+        mAdapter.add(P2);
+        mAdapter.add(P3);
+        assertContentsInOrder(mAdapter, P3, P2, P1);
+        mAdapter.removeItems(0, 2);
+        assertContentsInOrder(mAdapter, P1);
+        mAdapter.add(P2);
+        mAdapter.add(P3);
+        mAdapter.addExtraItem(EXTRA);
+        assertContentsInOrder(mAdapter, P3, P2, P1, EXTRA);
+        mAdapter.removeItems(1, 1);
+        assertContentsInOrder(mAdapter, P3, P1, EXTRA);
+        mAdapter.removeItems(1, 2);
+        assertContentsInOrder(mAdapter, P3);
+        mAdapter.addExtraItem(EXTRA);
+        mAdapter.addExtraItem(P2);
+        mAdapter.add(P1);
+        assertContentsInOrder(mAdapter, P3, P1, EXTRA, P2);
+        mAdapter.removeItems(1, 2);
+        assertContentsInOrder(mAdapter, P3, P2);
+        mAdapter.add(P1);
+        assertContentsInOrder(mAdapter, P3, P1, P2);
+    }
+
+    @Test
+    public void testReplace() {
+        mAdapter.add(P1);
+        mAdapter.add(P2);
+        assertNotEmpty();
+        assertContentsInOrder(mAdapter, P2, P1);
+        mAdapter.replace(1, P3);
+        assertContentsInOrder(mAdapter, P3, P2);
+        mAdapter.replace(0, P1);
+        assertContentsInOrder(mAdapter, P2, P1);
+        mAdapter.addExtraItem(EXTRA);
+        assertContentsInOrder(mAdapter, P2, P1, EXTRA);
+        mAdapter.replace(2, P3);
+        assertContentsInOrder(mAdapter, P2, P1, P3);
+    }
+
+    @Test
+    public void testChange_sorting() {
+        TestData p2_changed = TestData.create(2, "z changed");
+        mAdapter.add(P1);
+        mAdapter.add(P2);
+        assertNotEmpty();
+        assertContentsInOrder(mAdapter, P2, P1);
+        mAdapter.change(p2_changed);
+        assertContentsInOrder(mAdapter, P1, p2_changed);
+    }
+
+    @Test
+    public void testChange_new() {
+        mAdapter.change(P1);
+        assertNotEmpty();
+        assertContentsInOrder(mAdapter, P1);
+    }
+
+    private void assertEmpty() {
+        assertWithMessage("empty").that(mAdapter.isEmpty()).isTrue();
+    }
+
+    private void assertNotEmpty() {
+        assertWithMessage("empty").that(mAdapter.isEmpty()).isFalse();
+    }
+
+    private static void assertContentsInOrder(ObjectAdapter adapter, Object... contents) {
+        int ex = contents.length;
+        assertWithMessage("size").that(adapter.size()).isEqualTo(ex);
+        for (int i = 0; i < ex; i++) {
+            assertWithMessage("element " + 1).that(adapter.get(i)).isEqualTo(contents[i]);
+        }
+    }
+
+    private static class TestData {
+        @Override
+        public String toString() {
+            return "TestData[" + mId + "]{" + mText + '}';
+        }
+
+        static TestData create(long first, String text) {
+            return new TestData(first, text);
+        }
+
+        private final long mId;
+        private final String mText;
+
+        private TestData(long id, String second) {
+            this.mId = id;
+            this.mText = second;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof TestData)) return false;
+            TestData that = (TestData) o;
+            return mId == that.mId && Objects.equals(mText, that.mText);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mId, mText);
+        }
+    }
+
+    private static class TestSortedArrayAdapter extends SortedArrayAdapter<TestData> {
+
+        private static final Comparator<TestData> TEXT_COMPARATOR =
+                new Comparator<TestData>() {
+                    @Override
+                    public int compare(TestData lhs, TestData rhs) {
+                        return lhs.mText.compareTo(rhs.mText);
+                    }
+                };
+
+        TestSortedArrayAdapter(int maxInitialCount, Object extra) {
+            super(new ClassPresenterSelector(), TEXT_COMPARATOR, maxInitialCount);
+            if (extra != null) {
+                addExtraItem((TestData) extra);
+            }
+        }
+
+        @Override
+        protected long getId(TestData item) {
+            return item.mId;
+        }
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/dvr/ui/browse/DvrBrowseFragmentTest.java b/tests/robotests/src/com/android/tv/dvr/ui/browse/DvrBrowseFragmentTest.java
new file mode 100644
index 0000000..9b44117
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/dvr/ui/browse/DvrBrowseFragmentTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui.browse;
+
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.testing.ComparatorTester;
+import com.android.tv.testing.constants.ConfigConstants;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** Test for {@link DvrBrowseFragment}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class DvrBrowseFragmentTest {
+
+    @Test
+    public void testRecentRowComparator_scheduledRecordings_latestFirst() {
+        ComparatorTester<Object> comparatorTester =
+                ComparatorTester.withoutEqualsTest(DvrBrowseFragment.RECENT_ROW_COMPARATOR);
+        // priority (highest to lowest): start time, class, ID
+        comparatorTester.addComparableGroup(buildRecordedProgramForTest(2, 120, 150));
+        comparatorTester.addComparableGroup(buildRecordedProgramForTest(1, 120, 150));
+        comparatorTester.addComparableGroup(buildScheduledRecordingForTest(1, 120, 150));
+        comparatorTester.addComparableGroup(buildScheduledRecordingForTest(2, 120, 150));
+        comparatorTester.addComparableGroup(buildRecordedProgramForTest(4, 100, 200));
+        comparatorTester.addComparableGroup(buildRecordedProgramForTest(3, 100, 200));
+        comparatorTester.addComparableGroup(buildScheduledRecordingForTest(3, 100, 200));
+        comparatorTester.addComparableGroup(buildScheduledRecordingForTest(4, 100, 200));
+        comparatorTester.addComparableGroup(new Object(), Long.valueOf("777"), "string");
+        comparatorTester.test();
+    }
+
+    private ScheduledRecording buildScheduledRecordingForTest(long id, long start, long end) {
+        return ScheduledRecording.builder("test", 1, start, end).setId(id).build();
+    }
+
+    private RecordedProgram buildRecordedProgramForTest(long id, long start, long end) {
+        return RecordedProgram.builder()
+                .setId(id)
+                .setInputId("test")
+                .setStartTimeUtcMillis(start)
+                .setEndTimeUtcMillis(end)
+                .build();
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/dvr/ui/list/DvrHistoryRowAdapterTest.java b/tests/robotests/src/com/android/tv/dvr/ui/list/DvrHistoryRowAdapterTest.java
new file mode 100644
index 0000000..a317a76
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/dvr/ui/list/DvrHistoryRowAdapterTest.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui.list;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.leanback.widget.ClassPresenterSelector;
+import com.android.tv.common.flags.impl.DefaultUiFlags;
+import com.android.tv.common.util.Clock;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.android.tv.testing.dvr.DvrDataManagerInMemoryImpl;
+import com.android.tv.testing.fakes.FakeClock;
+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;
+
+/** Test for {@link DvrHistoryRowAdapter}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK, application = TestSingletonApp.class)
+public class DvrHistoryRowAdapterTest {
+
+    private static final ScheduledRecording SCHEDULE_1 =
+            buildScheduledRecordingForTest(
+                    2,
+                    1518249600000L, // 2/10/2018 0:00 PST
+                    1518250600000L,
+                    ScheduledRecording.STATE_RECORDING_FAILED);
+    private static final ScheduledRecording SCHEDULE_1_COPY =
+            buildScheduledRecordingForTest(
+                    3,
+                    1518249600000L, // 2/10/2018 0:00 PST
+                    1518250600000L,
+                    ScheduledRecording.STATE_RECORDING_FAILED);
+    private static final ScheduledRecording SCHEDULE_2 =
+            buildScheduledRecordingForTest(
+                    4,
+                    1518595200000L, // 2/14/2018 0:00 PST
+                    1518596200000L,
+                    ScheduledRecording.STATE_RECORDING_FAILED);
+    private static final ScheduledRecording SCHEDULE_2_COPY =
+            buildScheduledRecordingForTest(
+                    5,
+                    1518595200000L, // 2/14/2018 0:00 PST
+                    1518596200000L,
+                    ScheduledRecording.STATE_RECORDING_FAILED);
+
+    private static RecordedProgram sRecordedProgram;
+    private static final long FAKE_CURRENT_TIME = 1518764400000L; // 2/15/2018 23:00 PST
+
+    private DvrHistoryRowAdapter mDvrHistoryRowAdapter;
+    private DvrDataManagerInMemoryImpl mDvrDataManager;
+
+    @Before
+    public void setUp() {
+        sRecordedProgram =
+                buildRecordedProgramForTest(
+                        6,
+                        1518249600000L, // 2/10/2018 0:00 PST
+                        1518250600000L);
+
+        TestSingletonApp app = (TestSingletonApp) RuntimeEnvironment.application;
+        Clock clock = FakeClock.createWithTime(FAKE_CURRENT_TIME);
+
+        mDvrDataManager = new DvrDataManagerInMemoryImpl(app, clock);
+        app.mDvrDataManager = mDvrDataManager;
+        // keep the original IDs instead of creating a new one.
+        mDvrDataManager.addScheduledRecording(
+                true, SCHEDULE_1, SCHEDULE_1_COPY, SCHEDULE_2, SCHEDULE_2_COPY);
+
+        ClassPresenterSelector presenterSelector = new ClassPresenterSelector();
+        mDvrHistoryRowAdapter =
+                new DvrHistoryRowAdapter(RuntimeEnvironment.application, presenterSelector, clock,
+                    mDvrDataManager, new DefaultUiFlags());
+    }
+
+    @Test
+    public void testStart() {
+        mDvrHistoryRowAdapter.start();
+        assertInitialState();
+    }
+
+    @Test
+    public void testOnScheduledRecordingAdded_existingHeader() {
+        mDvrHistoryRowAdapter.start();
+        ScheduledRecording toAdd =
+                buildScheduledRecordingForTest(
+                        6,
+                        1518249600000L, // 2/10/2018
+                        1518250600000L,
+                        ScheduledRecording.STATE_RECORDING_FAILED);
+        mDvrHistoryRowAdapter.onScheduledRecordingAdded(toAdd);
+
+        // a schedule row is added
+        assertThat(mDvrHistoryRowAdapter.size()).isEqualTo(7);
+        assertThat(getHeaderItemCounts()).containsExactly(2, 3).inOrder();
+        assertThat(getScheduleSize()).isEqualTo(5);
+    }
+
+    @Test
+    public void testOnScheduledRecordingAdded_newHeader_addOldest() {
+        mDvrHistoryRowAdapter.start();
+        ScheduledRecording toAdd =
+                buildScheduledRecordingForTest(
+                        6,
+                        1518159600000L, // 2/8/2018 PST
+                        1518160600000L,
+                        ScheduledRecording.STATE_RECORDING_FAILED);
+        mDvrHistoryRowAdapter.onScheduledRecordingAdded(toAdd);
+
+        // a header row and a schedule row are added
+        assertThat(mDvrHistoryRowAdapter.size()).isEqualTo(8);
+        assertThat(getHeaderItemCounts()).containsExactly(2, 2, 1).inOrder();
+        assertThat(getScheduleSize()).isEqualTo(5);
+    }
+
+    @Test
+    public void testOnScheduledRecordingAdded_newHeader_addBetween() {
+        mDvrHistoryRowAdapter.start();
+        ScheduledRecording toAdd =
+                buildScheduledRecordingForTest(
+                        6,
+                        1518336000000L, // 2/11/2018 PST
+                        1518337000000L,
+                        ScheduledRecording.STATE_RECORDING_FAILED);
+        mDvrHistoryRowAdapter.onScheduledRecordingAdded(toAdd);
+
+        // a header row and a schedule row are added
+        assertThat(mDvrHistoryRowAdapter.size()).isEqualTo(8);
+        assertThat(getHeaderItemCounts()).containsExactly(2, 1, 2).inOrder();
+        assertThat(getScheduleSize()).isEqualTo(5);
+    }
+
+    @Test
+    public void testOnScheduledRecordingAdded_newHeader_addNewest() {
+        mDvrHistoryRowAdapter.start();
+        ScheduledRecording toAdd =
+                buildScheduledRecordingForTest(
+                        6,
+                        1518681600000L, // 2/15/2018 PST
+                        1518682600000L,
+                        ScheduledRecording.STATE_RECORDING_FAILED);
+        mDvrHistoryRowAdapter.onScheduledRecordingAdded(toAdd);
+
+        // a header row and a schedule row are added
+        assertThat(mDvrHistoryRowAdapter.size()).isEqualTo(8);
+        assertThat(getHeaderItemCounts()).containsExactly(1, 2, 2).inOrder();
+        assertThat(getScheduleSize()).isEqualTo(5);
+    }
+
+    @Test
+    public void testOnScheduledRecordingAdded_addRecordedProgram() {
+        mDvrHistoryRowAdapter.start();
+        mDvrHistoryRowAdapter.onScheduledRecordingAdded(sRecordedProgram);
+
+        // a header row and a schedule row are added
+        assertThat(mDvrHistoryRowAdapter.size()).isEqualTo(7);
+        assertThat(getHeaderItemCounts()).containsExactly(2, 3).inOrder();
+        assertThat(getScheduleSize()).isEqualTo(5);
+    }
+
+    @Test
+    public void testOnScheduledRecordingRemoved_keepHeader() {
+        mDvrHistoryRowAdapter.start();
+        mDvrHistoryRowAdapter.onScheduledRecordingRemoved(SCHEDULE_1);
+
+        // a schedule row is removed
+        assertThat(mDvrHistoryRowAdapter.size()).isEqualTo(5);
+        assertThat(getHeaderItemCounts()).containsExactly(2, 1).inOrder();
+        assertThat(getScheduleSize()).isEqualTo(3);
+    }
+
+    @Test
+    public void testOnScheduledRecordingRemoved_removeHeader() {
+        mDvrHistoryRowAdapter.start();
+        mDvrHistoryRowAdapter.onScheduledRecordingRemoved(SCHEDULE_1);
+        mDvrHistoryRowAdapter.onScheduledRecordingRemoved(SCHEDULE_1_COPY);
+
+        // a header row and a schedule row are removed
+        assertThat(mDvrHistoryRowAdapter.size()).isEqualTo(3);
+        assertThat(getHeaderItemCounts()).containsExactly(2).inOrder();
+        assertThat(getScheduleSize()).isEqualTo(2);
+    }
+
+    @Test
+    public void testOnScheduledRecordingRemoved_removeRecordedProgram() {
+        mDvrDataManager.addRecordedProgramInternal(sRecordedProgram, true);
+        mDvrHistoryRowAdapter.start();
+        assertThat(mDvrHistoryRowAdapter.size()).isEqualTo(7);
+        assertThat(getHeaderItemCounts()).containsExactly(2, 3).inOrder();
+        assertThat(getScheduleSize()).isEqualTo(5);
+
+        mDvrHistoryRowAdapter.onScheduledRecordingRemoved(sRecordedProgram);
+
+        // a schedule row is removed
+        assertThat(mDvrHistoryRowAdapter.size()).isEqualTo(6);
+        assertThat(getHeaderItemCounts()).containsExactly(2, 2);
+        assertThat(getScheduleSize()).isEqualTo(4);
+    }
+
+    private static ScheduledRecording buildScheduledRecordingForTest(
+            long id, long startTime, long endTime, int state) {
+        ScheduledRecording.Builder builder =
+                ScheduledRecording.builder("fakeInput", 1, startTime, endTime)
+                        .setId(id)
+                        .setState(state);
+        return builder.build();
+    }
+
+    private static RecordedProgram buildRecordedProgramForTest(
+            long id, long startTime, long endTime) {
+        RecordedProgram.Builder builder =
+                RecordedProgram.builder()
+                        .setId(id)
+                        .setInputId("fakeInput")
+                        .setStartTimeUtcMillis(startTime)
+                        .setEndTimeUtcMillis(endTime);
+        return builder.build();
+    }
+
+    private int getScheduleSize() {
+        int size = 0;
+        for (int i = 0; i < mDvrHistoryRowAdapter.size(); i++) {
+            Object item = mDvrHistoryRowAdapter.get(i);
+            if (item instanceof ScheduleRow && ((ScheduleRow) item).getSchedule() != null) {
+                size++;
+            }
+        }
+        return size;
+    }
+
+    private List<Integer> getHeaderItemCounts() {
+        List<Integer> result = new ArrayList<>();
+        for (int i = 0; i < mDvrHistoryRowAdapter.size(); i++) {
+            Object item = mDvrHistoryRowAdapter.get(i);
+            if (item instanceof SchedulesHeaderRow) {
+                int count = ((SchedulesHeaderRow) item).getItemCount();
+                assertThat(count).isAtLeast(1);
+                result.add(count);
+            }
+        }
+        return result;
+    }
+
+    private void assertInitialState() {
+        assertThat(mDvrHistoryRowAdapter.size()).isEqualTo(6);
+        assertThat(getHeaderItemCounts()).containsExactly(2, 2).inOrder();
+        assertThat(getScheduleSize()).isEqualTo(4);
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/dvr/ui/playback/DvrPlayerTest.java b/tests/robotests/src/com/android/tv/dvr/ui/playback/DvrPlayerTest.java
new file mode 100644
index 0000000..28069b6
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/dvr/ui/playback/DvrPlayerTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui.playback;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.media.tv.TvTrackInfo;
+import com.android.tv.ShadowTvView;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.android.tv.ui.AppLayerTvView;
+import java.util.ArrayList;
+import java.util.Collections;
+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;
+import org.robolectric.shadow.api.Shadow;
+
+/** Test for {@link DvrPlayer}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        sdk = ConfigConstants.SDK,
+        application = TestSingletonApp.class,
+        shadows = {ShadowTvView.class})
+public class DvrPlayerTest {
+    private ShadowTvView mShadowTvView;
+    private DvrPlayer mDvrPlayer;
+
+    @Before
+    public void setUp() {
+        AppLayerTvView tvView = new AppLayerTvView(RuntimeEnvironment.application);
+        mShadowTvView = Shadow.extract(tvView);
+        mDvrPlayer = new DvrPlayer(tvView, RuntimeEnvironment.application);
+    }
+
+    @Test
+    public void  testGetAudioTracks_null() {
+        mShadowTvView.mTracks.put(TvTrackInfo.TYPE_AUDIO, null);
+        assertThat(mDvrPlayer.getAudioTracks()).isNotNull();
+        assertThat(mDvrPlayer.getAudioTracks()).isEmpty();
+    }
+
+    @Test
+    public void  testGetAudioTracks_empty() {
+        mShadowTvView.mTracks.put(TvTrackInfo.TYPE_AUDIO, new ArrayList<>());
+        assertThat(mDvrPlayer.getAudioTracks()).isNotNull();
+        assertThat(mDvrPlayer.getAudioTracks()).isEmpty();
+    }
+
+    @Test
+    public void  testGetAudioTracks_nonEmpty() {
+        TvTrackInfo info = new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, "fake id").build();
+        mShadowTvView.mTracks.put(TvTrackInfo.TYPE_AUDIO, Collections.singletonList(info));
+        assertThat(mDvrPlayer.getAudioTracks()).containsExactly(info);
+    }
+
+}
diff --git a/tests/robotests/src/com/android/tv/guide/ProgramItemViewTest.java b/tests/robotests/src/com/android/tv/guide/ProgramItemViewTest.java
new file mode 100644
index 0000000..e7850c1
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/guide/ProgramItemViewTest.java
@@ -0,0 +1,288 @@
+/*
+ * 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.guide;
+
+import static com.google.android.libraries.testing.truth.TextViewSubject.assertThat;
+
+import android.support.annotation.NonNull;
+import android.view.LayoutInflater;
+
+import com.android.tv.R;
+import com.android.tv.common.util.Clock;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.ProgramImpl;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.guide.ProgramItemViewTest.TestApp;
+import com.android.tv.guide.ProgramItemViewTest.TestModule.Contributes;
+import com.android.tv.guide.ProgramManager.TableEntry;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.android.tv.testing.robo.RobotTestAppHelper;
+import com.android.tv.testing.testdata.TestData;
+
+import dagger.Component;
+import dagger.Module;
+import dagger.Provides;
+import dagger.android.AndroidInjectionModule;
+import dagger.android.AndroidInjector;
+import dagger.android.ContributesAndroidInjector;
+import dagger.android.DispatchingAndroidInjector;
+import dagger.android.HasAndroidInjector;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+import javax.inject.Inject;
+
+/** Tests for {@link ProgramItemView}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK, application = TestApp.class)
+public class ProgramItemViewTest {
+
+    /** TestApp for {@link ProgramItemViewTest} */
+    public static class TestApp extends TestSingletonApp implements HasAndroidInjector {
+        @Inject DispatchingAndroidInjector<Object> dispatchingAndroidInjector;
+
+        @Override
+        public void onCreate() {
+            super.onCreate();
+            DaggerProgramItemViewTest_TestAppComponent.builder()
+                    .testModule(new TestModule(this))
+                    .build()
+                    .inject(this);
+        }
+
+        @Override
+        public AndroidInjector<Object> androidInjector() {
+            return dispatchingAndroidInjector;
+        }
+    }
+
+    /** Component for {@link ProgramItemViewTest} */
+    @Component(
+            modules = {
+                AndroidInjectionModule.class,
+                TestModule.class,
+            })
+    interface TestAppComponent extends AndroidInjector<TestApp> {}
+
+    /** Module for {@link ProgramItemViewTest} */
+    @Module(includes = {Contributes.class})
+    public static class TestModule {
+
+        @Module()
+        public abstract static class Contributes {
+            @ContributesAndroidInjector
+            abstract ProgramItemView contributesProgramItemView();
+        }
+
+        private final TestApp myTestApp;
+
+        TestModule(TestApp test) {
+            myTestApp = test;
+        }
+
+        @Provides
+        ChannelDataManager providesChannelDataManager() {
+            return myTestApp.getChannelDataManager();
+        }
+
+        @Provides
+        Clock provideClock() {
+            return myTestApp.getClock();
+        }
+    }
+
+    //  Thursday, June 1, 2017 1:00:00 PM GMT-07:00
+    private final long testStartTimeMs = 1496347200000L;
+
+    // Thursday, June 1, 2017 8:00:00 PM GMT-07:00
+    private final long eightPM = 1496372400000L;
+    private final ProgramImpl baseProgram =
+            new ProgramImpl.Builder()
+                    .setChannelId(1)
+                    .setStartTimeUtcMillis(eightPM)
+                    .setEndTimeUtcMillis(eightPM + Duration.ofHours(1).toMillis())
+                    .build();
+
+    private ProgramItemView mPprogramItemView;
+
+    @Mock DvrManager dvrManager;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        TestSingletonApp app = (TestSingletonApp) RuntimeEnvironment.application;
+        app.dvrManager = dvrManager;
+        app.fakeClock.setBootTimeMillis(testStartTimeMs + TimeUnit.HOURS.toMillis(-12));
+        app.fakeClock.setCurrentTimeMillis(testStartTimeMs);
+        RobotTestAppHelper.loadTestData(app, TestData.DEFAULT_10_CHANNELS);
+        mPprogramItemView =
+                (ProgramItemView)
+                        LayoutInflater.from(RuntimeEnvironment.application)
+                                .inflate(R.layout.program_guide_table_item, null);
+        GuideUtils.setWidthPerHour(100);
+    }
+
+    @Test
+    public void initialState() {
+        assertThat(mPprogramItemView).hasEmptyText();
+    }
+
+    @Test
+    public void setValue_noProgram() {
+        TableEntry tableEntry = create30MinuteTableEntryFor(null, null, false);
+        mPprogramItemView.setValues(null, tableEntry, 0, 0, 0, "a gap");
+        assertThat(mPprogramItemView).hasText("a gap");
+        assertThat(mPprogramItemView).hasContentDescription("1 a gap 8:00 – 9:00 PM");
+    }
+
+    @Test
+    public void setValue_programNoTitle() {
+        ProgramImpl program = new ProgramImpl.Builder(baseProgram).build();
+        TableEntry tableEntry = create30MinuteTableEntryFor(program, null, false);
+        mPprogramItemView.setValues(null, tableEntry, 0, 0, 0, "a gap");
+        assertThat(mPprogramItemView).hasText("No information");
+        assertThat(mPprogramItemView).hasContentDescription("1 No information 8:00 – 9:00 PM");
+    }
+
+    @Test
+    public void setValue_programTitle() {
+        ProgramImpl program =
+                new ProgramImpl.Builder(baseProgram).setTitle("A good program").build();
+        TableEntry tableEntry = create30MinuteTableEntryFor(program, null, false);
+        mPprogramItemView.setValues(null, tableEntry, 0, 0, 0, "a gap");
+        assertThat(mPprogramItemView).hasText("A good program");
+        assertThat(mPprogramItemView).hasContentDescription("1 A good program 8:00 – 9:00 PM");
+    }
+
+    @Test
+    public void setValue_programDescriptionBlocked() {
+        ProgramImpl program =
+                new ProgramImpl.Builder(baseProgram)
+                        .setTitle("A good program")
+                        .setDescription("Naughty")
+                        .build();
+        TableEntry tableEntry = create30MinuteTableEntryFor(program, null, true);
+        mPprogramItemView.setValues(null, tableEntry, 0, 0, 0, "a gap");
+        assertThat(mPprogramItemView).hasText("A good program");
+        assertThat(mPprogramItemView)
+                .hasContentDescription("1 A good program 8:00 – 9:00 PM This program is blocked");
+    }
+
+    @Test
+    public void setValue_programEpisode() {
+        ProgramImpl program =
+                new ProgramImpl.Builder(baseProgram)
+                        .setTitle("A good program")
+                        .setEpisodeTitle("The one with an episode")
+                        .build();
+        TableEntry tableEntry = create30MinuteTableEntryFor(program, null, false);
+        mPprogramItemView.setValues(null, tableEntry, 0, 0, 0, "a gap");
+        assertThat(mPprogramItemView).hasText("A good program\n\u200DThe one with an episode");
+        assertThat(mPprogramItemView)
+                .hasContentDescription("1 A good program 8:00 – 9:00 PM The one with an episode");
+    }
+
+    @Test
+    public void setValue_programEpisodeAndDescrition() {
+        ProgramImpl program =
+                new ProgramImpl.Builder(baseProgram)
+                        .setTitle("A good program")
+                        .setEpisodeTitle("The one with an episode")
+                        .setDescription("Jack and Jill go up a hill")
+                        .build();
+        TableEntry tableEntry = create30MinuteTableEntryFor(program, null, false);
+        mPprogramItemView.setValues(null, tableEntry, 0, 0, 0, "a gap");
+        assertThat(mPprogramItemView).hasText("A good program\n\u200DThe one with an episode");
+        assertThat(mPprogramItemView)
+                .hasContentDescription(
+                        "1 A good program 8:00 – 9:00 PM The one with an episode"
+                                + " Jack and Jill go up a hill");
+    }
+
+    @Test
+    public void setValue_scheduledConflict() {
+        ProgramImpl program =
+                new ProgramImpl.Builder(baseProgram).setTitle("A good program").build();
+        ScheduledRecording scheduledRecording =
+                ScheduledRecording.builder("input1", program).build();
+        TableEntry tableEntry = create30MinuteTableEntryFor(program, scheduledRecording, false);
+        Mockito.when(dvrManager.isConflicting(scheduledRecording)).thenReturn(true);
+
+        mPprogramItemView.setValues(null, tableEntry, 0, 0, 0, "a gap");
+        assertThat(mPprogramItemView).hasText("A good program");
+        assertThat(mPprogramItemView)
+                .hasContentDescription("1 A good program 8:00 – 9:00 PM Recording conflict");
+    }
+
+    @Test
+    public void setValue_scheduled() {
+        ProgramImpl program =
+                new ProgramImpl.Builder(baseProgram).setTitle("A good program").build();
+        ScheduledRecording scheduledRecording =
+                ScheduledRecording.builder("input1", program)
+                        .setState(ScheduledRecording.STATE_RECORDING_NOT_STARTED)
+                        .build();
+        TableEntry tableEntry = create30MinuteTableEntryFor(program, scheduledRecording, false);
+        Mockito.when(dvrManager.isConflicting(scheduledRecording)).thenReturn(false);
+
+        mPprogramItemView.setValues(null, tableEntry, 0, 0, 0, "a gap");
+        assertThat(mPprogramItemView).hasText("A good program");
+        assertThat(mPprogramItemView)
+                .hasContentDescription("1 A good program 8:00 – 9:00 PM Recording scheduled");
+    }
+
+    @Test
+    public void setValue_recordingInProgress() {
+        ProgramImpl program =
+                new ProgramImpl.Builder(baseProgram).setTitle("A good program").build();
+        ScheduledRecording scheduledRecording =
+                ScheduledRecording.builder("input1", program)
+                        .setState(ScheduledRecording.STATE_RECORDING_IN_PROGRESS)
+                        .build();
+        TableEntry tableEntry = create30MinuteTableEntryFor(program, scheduledRecording, false);
+        Mockito.when(dvrManager.isConflicting(scheduledRecording)).thenReturn(false);
+
+        mPprogramItemView.setValues(null, tableEntry, 0, 0, 0, "a gap");
+        assertThat(mPprogramItemView).hasText("A good program");
+        assertThat(mPprogramItemView)
+                .hasContentDescription("1 A good program 8:00 – 9:00 PM Recording");
+    }
+
+    @NonNull
+    private TableEntry create30MinuteTableEntryFor(
+            ProgramImpl program, ScheduledRecording scheduledRecording, boolean isBlocked) {
+        return ProgramManager.createTableEntryForTest(
+                1,
+                program,
+                scheduledRecording,
+                eightPM,
+                eightPM + Duration.ofHours(1).toMillis(),
+                isBlocked);
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/guide/ProgramTableAdapterTest.java b/tests/robotests/src/com/android/tv/guide/ProgramTableAdapterTest.java
new file mode 100644
index 0000000..5207274
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/guide/ProgramTableAdapterTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.guide;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyLong;
+
+import com.android.tv.common.flags.impl.DefaultUiFlags;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.ChannelImpl;
+import com.android.tv.data.GenreItems;
+import com.android.tv.data.ProgramDataManager;
+import com.android.tv.data.ProgramImpl;
+import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.constants.ConfigConstants;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/** Tests for {@link ProgramTableAdapter}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK, application = TestSingletonApp.class)
+public class ProgramTableAdapterTest {
+
+    @Mock private ProgramGuide mProgramGuide;
+    @Mock private ChannelDataManager mChannelDataManager;
+    @Mock private ProgramDataManager mProgramDataManager;
+    private ProgramManager mProgramManager;
+
+    //  Thursday, June 1, 2017 1:00:00 PM GMT-07:00
+    private final long mTestStartTimeMs = 1496347200000L;
+    // Thursday, June 1, 2017 8:00:00 PM GMT-07:00
+    private final long mEightPM = 1496372400000L;
+    private DefaultUiFlags mUiFlags;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        TestSingletonApp app = (TestSingletonApp) RuntimeEnvironment.application;
+        app.fakeClock.setBootTimeMillis(mTestStartTimeMs + TimeUnit.HOURS.toMillis(-12));
+        app.fakeClock.setCurrentTimeMillis(mTestStartTimeMs);
+        mUiFlags = new DefaultUiFlags();
+        mProgramManager =
+                new ProgramManager(
+                        app.getTvInputManagerHelper(),
+                        mChannelDataManager,
+                        mProgramDataManager,
+                        null,
+                        null);
+    }
+
+    @Test
+    public void testOnTableEntryChanged() {
+        Mockito.when(mProgramGuide.getProgramManager()).thenReturn(mProgramManager);
+        Mockito.when(mProgramDataManager.getCurrentProgram(anyLong()))
+                .thenAnswer(
+                        invocation -> {
+                            long id = (long) invocation.getArguments()[0];
+                            return buildProgramForTesting(
+                                    id, id, (int) id % GenreItems.getGenreCount());
+                        });
+        ProgramTableAdapter programTableAdapter =
+                new ProgramTableAdapter(RuntimeEnvironment.application, mProgramGuide, mUiFlags);
+        mProgramManager.setChannels(buildChannelForTesting(1, 2, 3));
+        assertThat(mProgramManager.getChannelCount()).isEqualTo(3);
+
+        // set genre ID to 1. Then channel 1 is in the filtered list but channel 2 is not.
+        mProgramManager.resetChannelListWithGenre(1);
+        assertThat(mProgramManager.getChannelCount()).isEqualTo(1);
+        assertThat(mProgramManager.getChannelIndex(2)).isEqualTo(-1);
+
+        // should be no exception when onTableEntryChanged() is called
+        programTableAdapter.onTableEntryChanged(
+                ProgramManager.createTableEntryForTest(
+                        2,
+                        mProgramDataManager.getCurrentProgram(2),
+                        null,
+                        mTestStartTimeMs,
+                        mEightPM,
+                        false));
+    }
+
+    private List<Channel> buildChannelForTesting(long... ids) {
+        List<Channel> channels = new ArrayList<>();
+        for (long id : ids) {
+            channels.add(new ChannelImpl.Builder().setId(id).build());
+        }
+        return channels;
+    }
+
+    private Program buildProgramForTesting(long id, long channelId, int genreId) {
+        return new ProgramImpl.Builder()
+                .setId(id)
+                .setChannelId(channelId)
+                .setCanonicalGenres(GenreItems.getCanonicalGenre(genreId))
+                .build();
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/search/FakeSearchInterface.java b/tests/robotests/src/com/android/tv/search/FakeSearchInterface.java
new file mode 100644
index 0000000..568bddd
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/search/FakeSearchInterface.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.search;
+
+import android.content.Intent;
+import android.media.tv.TvContract;
+import android.media.tv.TvContract.Programs;
+
+import com.android.tv.data.api.Program;
+import com.android.tv.search.LocalSearchProvider.SearchResult;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Fake {@link SearchInterface} for testing. */
+public class FakeSearchInterface implements SearchInterface {
+    public final List<Program> mPrograms = new ArrayList<>();
+
+    @Override
+    public List<SearchResult> search(String query, int limit, int action) {
+
+        List<SearchResult> results = new ArrayList<>();
+        for (Program program : mPrograms) {
+            if (program.getTitle().contains(query) || program.getDescription().contains(query)) {
+                results.add(fromProgram(program));
+            }
+        }
+        return results;
+    }
+
+    public static SearchResult fromProgram(Program program) {
+        SearchResult.Builder result = SearchResult.builder();
+        result.setTitle(program.getTitle());
+        result.setDescription(
+                program.getStartTimeUtcMillis() + " - " + program.getEndTimeUtcMillis());
+        result.setImageUri(program.getPosterArtUri());
+        result.setIntentAction(Intent.ACTION_VIEW);
+        result.setIntentData(TvContract.buildChannelUri(program.getChannelId()).toString());
+        result.setIntentExtraData(TvContract.buildProgramUri(program.getId()).toString());
+        result.setContentType(Programs.CONTENT_ITEM_TYPE);
+        result.setIsLive(true);
+        result.setVideoWidth(program.getVideoWidth());
+        result.setVideoHeight(program.getVideoHeight());
+        result.setDuration(program.getDurationMillis());
+        result.setChannelId(program.getChannelId());
+        return result.build();
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/search/LocalSearchProviderTest.java b/tests/robotests/src/com/android/tv/search/LocalSearchProviderTest.java
new file mode 100644
index 0000000..caceb3f
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/search/LocalSearchProviderTest.java
@@ -0,0 +1,257 @@
+/*
+ * 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.search;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.Assert.fail;
+
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+
+import android.app.SearchManager;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.database.Cursor;
+import android.net.Uri;
+
+import com.android.tv.common.flags.impl.SettableFlagsModule;
+import com.android.tv.data.ProgramImpl;
+import com.android.tv.data.api.Program;
+import com.android.tv.perf.PerformanceMonitor;
+import com.android.tv.perf.stub.StubPerformanceMonitor;
+import com.android.tv.search.LocalSearchProvider.SearchResult;
+import com.android.tv.search.LocalSearchProviderTest.TestAppComponent.TestAppModule;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.android.tv.testing.robo.ContentProviders;
+
+import dagger.Component;
+import dagger.Module;
+import dagger.Provides;
+import dagger.android.AndroidInjectionModule;
+import dagger.android.AndroidInjector;
+import dagger.android.DispatchingAndroidInjector;
+import dagger.android.HasAndroidInjector;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/** Unit test for {@link LocalSearchProvider}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK, application = LocalSearchProviderTest.TestApp.class)
+public class LocalSearchProviderTest {
+
+    /** Test app for {@link LocalSearchProviderTest} */
+    public static class TestApp extends TestSingletonApp implements HasAndroidInjector {
+        @Inject DispatchingAndroidInjector<Object> mDispatchingAndroidProvider;
+
+        @Override
+        public void onCreate() {
+            super.onCreate();
+            DaggerLocalSearchProviderTest_TestAppComponent.builder()
+                    .settableFlagsModule(flagsModule)
+                    .build()
+                    .inject(this);
+        }
+
+        @Override
+        public AndroidInjector<Object> androidInjector() {
+            return mDispatchingAndroidProvider;
+        }
+    }
+
+    @Component(
+            modules = {
+                AndroidInjectionModule.class,
+                SettableFlagsModule.class,
+                LocalSearchProvider.Module.class,
+                TestAppModule.class
+            })
+    @Singleton
+    interface TestAppComponent extends AndroidInjector<TestApp> {
+        @Module
+        abstract static class TestAppModule {
+            @Provides
+            @Singleton
+            static PerformanceMonitor providePerformanceMonitor() {
+                return new StubPerformanceMonitor();
+            }
+        }
+    }
+
+    private final Program mProgram1 =
+            new ProgramImpl.Builder()
+                    .setTitle("Dummy program")
+                    .setDescription("Dummy program season 2")
+                    .setPosterArtUri("FakeUri")
+                    .setStartTimeUtcMillis(1516674000000L)
+                    .setEndTimeUtcMillis(1516677000000L)
+                    .setChannelId(7)
+                    .setVideoWidth(1080)
+                    .setVideoHeight(960)
+                    .build();
+
+    private final String mAuthority = "com.google.android.tv.search";
+    private final String mKeyword = "mKeyword";
+    private final Uri mBaseSearchUri =
+            Uri.parse(
+                    "content://"
+                            + mAuthority
+                            + "/"
+                            + SearchManager.SUGGEST_URI_PATH_QUERY
+                            + "/"
+                            + mKeyword);
+
+    private final Uri mWrongSearchUri =
+            Uri.parse("content://" + mAuthority + "/wrong_path/" + mKeyword);
+
+    private LocalSearchProvider mProvider;
+    private ContentResolver mContentResolver;
+
+    @Mock private SearchInterface mMockSearchInterface;
+    private final FakeSearchInterface mFakeSearchInterface = new FakeSearchInterface();
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mProvider = ContentProviders.register(LocalSearchProvider.class, mAuthority);
+        mContentResolver = RuntimeEnvironment.application.getContentResolver();
+    }
+
+    @Test
+    public void testQuery_normalUri() {
+        verifyQueryWithArguments(null, null);
+        verifyQueryWithArguments(1, null);
+        verifyQueryWithArguments(null, 1);
+        verifyQueryWithArguments(1, 1);
+    }
+
+    @Test
+    public void testQuery_invalidUri() {
+        try (Cursor c = mContentResolver.query(mWrongSearchUri, null, null, null, null)) {
+            fail("Query with invalid URI should fail.");
+        } catch (IllegalArgumentException e) {
+            // Success.
+        }
+    }
+
+    @Test
+    public void testQuery_invalidLimit() {
+        verifyQueryWithArguments(-1, null);
+    }
+
+    @Test
+    public void testQuery_invalidAction() {
+        verifyQueryWithArguments(null, SearchInterface.ACTION_TYPE_START - 1);
+        verifyQueryWithArguments(null, SearchInterface.ACTION_TYPE_END + 1);
+    }
+
+    private void verifyQueryWithArguments(Integer limit, Integer action) {
+        mProvider.setSearchInterface(mMockSearchInterface);
+        Uri uri = mBaseSearchUri;
+        if (limit != null || action != null) {
+            Uri.Builder builder = uri.buildUpon();
+            if (limit != null) {
+                builder.appendQueryParameter(
+                        SearchManager.SUGGEST_PARAMETER_LIMIT, limit.toString());
+            }
+            if (action != null) {
+                builder.appendQueryParameter(
+                        LocalSearchProvider.SUGGEST_PARAMETER_ACTION, action.toString());
+            }
+            uri = builder.build();
+        }
+        try (Cursor c = mContentResolver.query(uri, null, null, null, null)) {
+            // Do nothing.
+        }
+        int expectedLimit =
+                limit == null || limit <= 0 ? LocalSearchProvider.DEFAULT_SEARCH_LIMIT : limit;
+        int expectedAction =
+                (action == null
+                                || action < SearchInterface.ACTION_TYPE_START
+                                || action > SearchInterface.ACTION_TYPE_END)
+                        ? LocalSearchProvider.DEFAULT_SEARCH_ACTION
+                        : action;
+        verify(mMockSearchInterface).search(mKeyword, expectedLimit, expectedAction);
+        reset(mMockSearchInterface);
+    }
+
+    @Test
+    public void testGetType() {
+        assertThat(mProvider.getType(mBaseSearchUri)).isEqualTo(SearchManager.SUGGEST_MIME_TYPE);
+    }
+
+    @Test
+    public void query_empty() {
+        mProvider.setSearchInterface(mFakeSearchInterface);
+        Cursor cursor = mContentResolver.query(mBaseSearchUri, null, null, null, null);
+        assertThat(cursor.moveToNext()).isFalse();
+    }
+
+    @Test
+    public void query_program1() {
+        mProvider.setSearchInterface(mFakeSearchInterface);
+        mFakeSearchInterface.mPrograms.add(mProgram1);
+        Uri uri =
+                Uri.parse(
+                        "content://"
+                                + mAuthority
+                                + "/"
+                                + SearchManager.SUGGEST_URI_PATH_QUERY
+                                + "/"
+                                + "Dummy");
+        Cursor cursor = mContentResolver.query(uri, null, null, null, null);
+        assertThat(fromCursor(cursor)).containsExactly(FakeSearchInterface.fromProgram(mProgram1));
+    }
+
+    private List<SearchResult> fromCursor(Cursor cursor) {
+        List<SearchResult> results = new ArrayList<>();
+        while (cursor.moveToNext()) {
+            SearchResult.Builder result = SearchResult.builder();
+            int i = 0;
+            result.setTitle(cursor.getString(i++));
+            result.setDescription(cursor.getString(i++));
+            result.setImageUri(cursor.getString(i++));
+            result.setIntentAction(cursor.getString(i++));
+            String intentData = cursor.getString(i++);
+            result.setIntentData(intentData);
+            result.setIntentExtraData(cursor.getString(i++));
+            result.setContentType(cursor.getString(i++));
+            result.setIsLive(cursor.getString(i++).equals("1"));
+            result.setVideoWidth(Integer.valueOf(cursor.getString(i++)));
+            result.setVideoHeight(Integer.valueOf(cursor.getString(i++)));
+            result.setDuration(Long.valueOf(cursor.getString(i++)));
+            result.setProgressPercentage(Integer.valueOf(cursor.getString(i)));
+            result.setChannelId(ContentUris.parseId(Uri.parse(intentData)));
+            results.add(result.build());
+        }
+        return results;
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/testing/TvRobolectricTestRunner.java b/tests/robotests/src/com/android/tv/testing/TvRobolectricTestRunner.java
new file mode 100644
index 0000000..1efbee1
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/testing/TvRobolectricTestRunner.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.testing;
+
+import java.util.List;
+import org.junit.runners.model.InitializationError;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.manifest.AndroidManifest;
+import org.robolectric.res.Fs;
+import org.robolectric.res.ResourcePath;
+
+/**
+ * Custom test runner TV. This is needed because the default behavior for robolectric is just to
+ * grab the resource directory in the target package. We want to override this to add several
+ * spanning different projects.
+ *
+ * <p><b>Note</b> copied from
+ * http://cs/android/packages/apps/Settings/tests/robotests/src/com/android/settings/testutils/SettingsRobolectricTestRunner.java
+ */
+public class TvRobolectricTestRunner extends RobolectricTestRunner {
+
+    /** We don't actually want to change this behavior, so we just call super. */
+    public TvRobolectricTestRunner(Class<?> testClass) throws InitializationError {
+        super(testClass);
+    }
+
+    /**
+     * We are going to create our own custom manifest so that we can add multiple resource paths to
+     * it. This lets us access resources in both Settings and SettingsLib in our tests.
+     */
+    @Override
+    protected AndroidManifest getAppManifest(Config config) {
+        // Using the manifest file's relative path, we can figure out the application directory.
+        final String appRoot = "vendor/unbundled_google/packages/TV";
+        final String manifestPath = appRoot + "/AndroidManifest.xml";
+        final String resDir = appRoot + "/tests/robotests/res";
+        final String assetsDir = appRoot + config.assetDir();
+
+        // By adding any resources from libraries we need the AndroidManifest, we can access
+        // them from within the parallel universe's resource loader.
+        final AndroidManifest manifest =
+                new AndroidManifest(
+                        Fs.fileFromPath(manifestPath),
+                        Fs.fileFromPath(resDir),
+                        Fs.fileFromPath(assetsDir)) {
+                    @Override
+                    public List<ResourcePath> getIncludedResourcePaths() {
+                        List<ResourcePath> paths = super.getIncludedResourcePaths();
+                        TvRobolectricTestRunner.getIncludedResourcePaths(getPackageName(), paths);
+                        return paths;
+                    }
+                };
+
+        // Set the package name to the renamed one
+        manifest.setPackageName("com.android.tv");
+        return manifest;
+    }
+
+    public static void getIncludedResourcePaths(String packageName, List<ResourcePath> paths) {
+        paths.add(
+                new ResourcePath(
+                        packageName,
+                        Fs.fileFromPath("./vendor/unbundled_google/packages/TV/res"),
+                        null));
+        paths.add(
+                new ResourcePath(
+                        packageName,
+                        Fs.fileFromPath("./vendor/unbundled_google/packages/TV/common/res"),
+                        null));
+        paths.add(
+                new ResourcePath(
+                        packageName,
+                        Fs.fileFromPath("./vendor/unbundled_google/packages/TV/tuner/res"),
+                        null));
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/ui/ChannelBannerViewTest.java b/tests/robotests/src/com/android/tv/ui/ChannelBannerViewTest.java
new file mode 100644
index 0000000..bb645a8
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/ui/ChannelBannerViewTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.ui;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.robolectric.Shadows.shadowOf;
+
+import android.app.Activity;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.android.tv.R;
+import com.android.tv.common.flags.impl.DefaultLegacyFlags;
+import com.android.tv.common.singletons.HasSingletons;
+import com.android.tv.data.api.Channel;
+import com.android.tv.data.api.Program;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.android.tv.ui.ChannelBannerView.MySingletons;
+import com.android.tv.util.TvInputManagerHelper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import javax.inject.Provider;
+
+/** Tests for {@link ChannelBannerView}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class ChannelBannerViewTest {
+
+    private TestActivity testActivity;
+    private ChannelBannerView mChannelBannerView;
+    private ImageView mChannelSignalStrengthView;
+
+    @Before
+    public void setUp() {
+        testActivity = Robolectric.buildActivity(TestActivity.class).create().get();
+        mChannelBannerView =
+                (ChannelBannerView)
+                        LayoutInflater.from(testActivity).inflate(R.layout.channel_banner, null);
+        mChannelSignalStrengthView = mChannelBannerView.findViewById(R.id.channel_signal_strength);
+    }
+
+    @Test
+    public void updateChannelSignalStrengthView_valueIsNotValid() {
+        mChannelBannerView.updateChannelSignalStrengthView(-1);
+        assertThat(mChannelSignalStrengthView.getVisibility()).isEqualTo(View.GONE);
+        mChannelBannerView.updateChannelSignalStrengthView(101);
+        assertThat(mChannelSignalStrengthView.getVisibility()).isEqualTo(View.GONE);
+    }
+
+    @Test
+    public void updateChannelSignalStrengthView_20() {
+        mChannelBannerView.updateChannelSignalStrengthView(20);
+        assertThat(mChannelSignalStrengthView.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(shadowOf(mChannelSignalStrengthView.getDrawable()).getCreatedFromResId())
+                .isEqualTo(R.drawable.quantum_ic_signal_cellular_0_bar_white_24);
+    }
+
+    @Test
+    public void updateChannelSignalStrengthView_40() {
+        mChannelBannerView.updateChannelSignalStrengthView(40);
+        assertThat(mChannelSignalStrengthView.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(shadowOf(mChannelSignalStrengthView.getDrawable()).getCreatedFromResId())
+                .isEqualTo(R.drawable.quantum_ic_signal_cellular_1_bar_white_24);
+    }
+
+    @Test
+    public void updateChannelSignalStrengthView_60() {
+        mChannelBannerView.updateChannelSignalStrengthView(60);
+        assertThat(mChannelSignalStrengthView.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(shadowOf(mChannelSignalStrengthView.getDrawable()).getCreatedFromResId())
+                .isEqualTo(R.drawable.quantum_ic_signal_cellular_2_bar_white_24);
+    }
+
+    @Test
+    public void updateChannelSignalStrengthView_80() {
+        mChannelBannerView.updateChannelSignalStrengthView(80);
+        assertThat(mChannelSignalStrengthView.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(shadowOf(mChannelSignalStrengthView.getDrawable()).getCreatedFromResId())
+                .isEqualTo(R.drawable.quantum_ic_signal_cellular_3_bar_white_24);
+    }
+
+    @Test
+    public void updateChannelSignalStrengthView_100() {
+        mChannelBannerView.updateChannelSignalStrengthView(100);
+        assertThat(mChannelSignalStrengthView.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(shadowOf(mChannelSignalStrengthView.getDrawable()).getCreatedFromResId())
+                .isEqualTo(R.drawable.quantum_ic_signal_cellular_4_bar_white_24);
+    }
+
+    /** An activity for {@link ChannelBannerViewTest}. */
+    public static class TestActivity extends Activity implements HasSingletons<MySingletons> {
+
+        MySingletonsImpl mSingletons = new MySingletonsImpl();
+        Context mContext = this;
+
+        @Override
+        public ChannelBannerView.MySingletons singletons() {
+            return mSingletons;
+        }
+
+        /** MySingletons implementation needed for this class. */
+        public class MySingletonsImpl implements ChannelBannerView.MySingletons {
+
+            @Override
+            public Provider<Channel> getCurrentChannelProvider() {
+                return null;
+            }
+
+            @Override
+            public Provider<Program> getCurrentProgramProvider() {
+                return null;
+            }
+
+            @Override
+            public Provider<TvOverlayManager> getOverlayManagerProvider() {
+                return null;
+            }
+
+            @Override
+            public TvInputManagerHelper getTvInputManagerHelperSingleton() {
+                return new TvInputManagerHelper(mContext, DefaultLegacyFlags.DEFAULT);
+            }
+
+            @Override
+            public Provider<Long> getCurrentPlayingPositionProvider() {
+                return null;
+            }
+
+            @Override
+            public DvrManager getDvrManagerSingleton() {
+                return null;
+            }
+        }
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/ui/hideable/AutoHideSchedulerTest.java b/tests/robotests/src/com/android/tv/ui/hideable/AutoHideSchedulerTest.java
new file mode 100644
index 0000000..6e30d72
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/ui/hideable/AutoHideSchedulerTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.ui.hideable;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.tv.testing.constants.ConfigConstants;
+import java.util.concurrent.TimeUnit;
+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;
+import org.robolectric.shadows.ShadowLooper;
+
+/** Test for {@link AutoHideScheduler}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class AutoHideSchedulerTest {
+
+    private TestRunnable mTestRunnable = new TestRunnable();
+    private AutoHideScheduler mAutoHideScheduler;
+    private ShadowLooper mShadowLooper;
+
+    @Before
+    public void setUp() throws Exception {
+        mShadowLooper = ShadowLooper.getShadowMainLooper();
+        mAutoHideScheduler = new AutoHideScheduler(RuntimeEnvironment.application, mTestRunnable);
+    }
+
+    @Test
+    public void initialState() {
+        assertThat(mAutoHideScheduler.isScheduled()).isFalse();
+    }
+
+    @Test
+    public void cancel() {
+        mAutoHideScheduler.cancel();
+        assertThat(mAutoHideScheduler.isScheduled()).isFalse();
+    }
+
+    @Test
+    public void schedule() {
+        mAutoHideScheduler.schedule(10);
+        assertThat(mAutoHideScheduler.isScheduled()).isTrue();
+        assertThat(mTestRunnable.runCount).isEqualTo(0);
+    }
+
+    @Test
+    public void setA11yEnabledThenSchedule() {
+        mAutoHideScheduler.onAccessibilityStateChanged(true);
+        mAutoHideScheduler.schedule(10);
+        assertThat(mAutoHideScheduler.isScheduled()).isFalse();
+        assertThat(mTestRunnable.runCount).isEqualTo(0);
+    }
+
+    @Test
+    public void scheduleThenCancel() {
+        mAutoHideScheduler.schedule(10);
+        assertThat(mAutoHideScheduler.isScheduled()).isTrue();
+        mAutoHideScheduler.cancel();
+        assertThat(mAutoHideScheduler.isScheduled()).isFalse();
+        assertThat(mTestRunnable.runCount).isEqualTo(0);
+    }
+
+    @Test
+    public void scheduleThenLoop() {
+        mAutoHideScheduler.schedule(10);
+        assertThat(mAutoHideScheduler.isScheduled()).isTrue();
+        assertThat(mTestRunnable.runCount).isEqualTo(0);
+        mShadowLooper.idle(9, TimeUnit.MILLISECONDS);
+        assertThat(mAutoHideScheduler.isScheduled()).isTrue();
+        assertThat(mTestRunnable.runCount).isEqualTo(0);
+        mShadowLooper.idle(1, TimeUnit.MILLISECONDS);
+        assertThat(mAutoHideScheduler.isScheduled()).isFalse();
+        assertThat(mTestRunnable.runCount).isEqualTo(1);
+    }
+
+    @Test
+    public void scheduleSetA11yEnabledThenLoop() {
+        mAutoHideScheduler.schedule(10);
+        assertThat(mAutoHideScheduler.isScheduled()).isTrue();
+        assertThat(mTestRunnable.runCount).isEqualTo(0);
+        mShadowLooper.idle(9, TimeUnit.MILLISECONDS);
+        assertThat(mAutoHideScheduler.isScheduled()).isTrue();
+        assertThat(mTestRunnable.runCount).isEqualTo(0);
+        mAutoHideScheduler.onAccessibilityStateChanged(true);
+        mShadowLooper.idle(1, TimeUnit.MILLISECONDS);
+        assertThat(mAutoHideScheduler.isScheduled()).isFalse();
+        assertThat(mTestRunnable.runCount).isEqualTo(0);
+    }
+
+    private static class TestRunnable implements Runnable {
+        int runCount = 0;
+
+        @Override
+        public void run() {
+            runCount++;
+        }
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/util/MultiLongSparseArrayTest.java b/tests/robotests/src/com/android/tv/util/MultiLongSparseArrayTest.java
new file mode 100644
index 0000000..8c51e49
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/util/MultiLongSparseArrayTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.tv.testing.constants.ConfigConstants;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link MultiLongSparseArray}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class MultiLongSparseArrayTest {
+    @Test
+    public void testEmpty() {
+        MultiLongSparseArray<String> sparseArray = new MultiLongSparseArray<>();
+        assertThat(sparseArray.get(0)).isEmpty();
+    }
+
+    @Test
+    public void testOneElement() {
+        MultiLongSparseArray<String> sparseArray = new MultiLongSparseArray<>();
+        sparseArray.put(0, "foo");
+        assertThat(sparseArray.get(0)).containsExactly("foo");
+    }
+
+    @Test
+    public void testTwoElements() {
+        MultiLongSparseArray<String> sparseArray = new MultiLongSparseArray<>();
+        sparseArray.put(0, "foo");
+        sparseArray.put(0, "bar");
+        assertThat(sparseArray.get(0)).containsExactly("foo", "bar");
+    }
+
+    @Test
+    public void testClearEmptyCache() {
+        MultiLongSparseArray<String> sparseArray = new MultiLongSparseArray<>();
+        sparseArray.clearEmptyCache();
+        assertThat(sparseArray.getEmptyCacheSize()).isEqualTo(0);
+        sparseArray.put(0, "foo");
+        sparseArray.remove(0, "foo");
+        assertThat(sparseArray.getEmptyCacheSize()).isEqualTo(1);
+        sparseArray.clearEmptyCache();
+        assertThat(sparseArray.getEmptyCacheSize()).isEqualTo(0);
+    }
+
+    @Test
+    public void testMaxEmptyCacheSize() {
+        MultiLongSparseArray<String> sparseArray = new MultiLongSparseArray<>();
+        sparseArray.clearEmptyCache();
+        assertThat(sparseArray.getEmptyCacheSize()).isEqualTo(0);
+        for (int i = 0; i <= MultiLongSparseArray.DEFAULT_MAX_EMPTIES_KEPT + 2; i++) {
+            sparseArray.put(i, "foo");
+        }
+        for (int i = 0; i <= MultiLongSparseArray.DEFAULT_MAX_EMPTIES_KEPT + 2; i++) {
+            sparseArray.remove(i, "foo");
+        }
+        assertThat(sparseArray.getEmptyCacheSize())
+                .isEqualTo(MultiLongSparseArray.DEFAULT_MAX_EMPTIES_KEPT);
+        sparseArray.clearEmptyCache();
+        assertThat(sparseArray.getEmptyCacheSize()).isEqualTo(0);
+    }
+
+    @Test
+    public void testReuseEmptySets() {
+        MultiLongSparseArray<String> sparseArray = new MultiLongSparseArray<>();
+        sparseArray.clearEmptyCache();
+        assertThat(sparseArray.getEmptyCacheSize()).isEqualTo(0);
+        // create a bunch of sets
+        for (int i = 0; i <= MultiLongSparseArray.DEFAULT_MAX_EMPTIES_KEPT + 2; i++) {
+            sparseArray.put(i, "foo");
+        }
+        // remove them so they are all put in the cache.
+        for (int i = 0; i <= MultiLongSparseArray.DEFAULT_MAX_EMPTIES_KEPT + 2; i++) {
+            sparseArray.remove(i, "foo");
+        }
+        assertThat(sparseArray.getEmptyCacheSize())
+                .isEqualTo(MultiLongSparseArray.DEFAULT_MAX_EMPTIES_KEPT);
+
+        // now create elements, that use the cached empty sets.
+        for (int i = 0; i < MultiLongSparseArray.DEFAULT_MAX_EMPTIES_KEPT; i++) {
+            sparseArray.put(10 + i, "bar");
+            assertThat(sparseArray.getEmptyCacheSize())
+                    .isEqualTo(MultiLongSparseArray.DEFAULT_MAX_EMPTIES_KEPT - i - 1);
+        }
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/util/TvInputManagerHelperRoboTest.java b/tests/robotests/src/com/android/tv/util/TvInputManagerHelperRoboTest.java
new file mode 100644
index 0000000..837a18b
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/util/TvInputManagerHelperRoboTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.media.tv.TvInputInfo;
+import android.media.tv.TvInputManager;
+import com.android.tv.common.flags.impl.DefaultLegacyFlags;
+import com.android.tv.testing.constants.ConfigConstants;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+/**
+ * Tests for {@link TvInputManagerHelper}.
+ *
+ * <p>This test is named ...RoboTest because there is already a test named <code>
+ * TvInputManagerHelperTest</code>
+ */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class TvInputManagerHelperRoboTest {
+
+    @Test
+    public void getInputState_null() {
+        TvInputInfo tvinputInfo = null;
+        TvInputManagerHelper tvInputManagerHelper =
+                new TvInputManagerHelper(
+                        RuntimeEnvironment.application, DefaultLegacyFlags.DEFAULT);
+    assertThat(TvInputManager.INPUT_STATE_DISCONNECTED)
+        .isSameInstanceAs(tvInputManagerHelper.getInputState(tvinputInfo));
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/util/TvProviderUtilsTest.java b/tests/robotests/src/com/android/tv/util/TvProviderUtilsTest.java
new file mode 100644
index 0000000..65734a8
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/util/TvProviderUtilsTest.java
@@ -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.
+ */
+
+package com.android.tv.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.pm.ProviderInfo;
+import android.media.tv.TvContract;
+import android.os.Bundle;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.android.tv.testing.fakes.FakeTvProvider;
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowContentResolver;
+
+/** Tests for {@link TvProviderUtils}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        sdk = ConfigConstants.SDK,
+        shadows = {ShadowContentResolver.class})
+public class TvProviderUtilsTest {
+
+    @Before
+    public void setUp() {
+        ProviderInfo info = new ProviderInfo();
+        info.authority = TvContract.AUTHORITY;
+        FakeTvProvider provider =
+                Robolectric.buildContentProvider(FakeTvProviderForTesting.class).create(info).get();
+        provider.onCreate();
+        ShadowContentResolver.registerProviderInternal(TvContract.AUTHORITY, provider);
+    }
+
+    @Test
+    public void testAddExtraColumnsToProjection() {
+        String[] inputStrings = {"column_1", "column_2", "column_3"};
+        assertThat(
+                TvProviderUtils
+                        .addExtraColumnsToProjection(
+                                inputStrings,
+                                TvProviderUtils.EXTRA_PROGRAM_COLUMN_STATE))
+                .asList()
+                .containsExactly(
+                        "column_1",
+                        "column_2",
+                        "column_3",
+                        TvProviderUtils.EXTRA_PROGRAM_COLUMN_STATE)
+                .inOrder();
+    }
+
+    @Test
+    public void testAddExtraColumnsToProjection_extraColumnExists() {
+        String[] inputStrings = {
+            "column_1",
+            "column_2",
+            TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID,
+            TvProviderUtils.EXTRA_PROGRAM_COLUMN_STATE,
+            "column_3"
+        };
+        assertThat(
+                TvProviderUtils
+                        .addExtraColumnsToProjection(
+                                inputStrings,
+                                TvProviderUtils.EXTRA_PROGRAM_COLUMN_STATE))
+                .asList()
+                .containsExactly(
+                        "column_1",
+                        "column_2",
+                        TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID,
+                        TvProviderUtils.EXTRA_PROGRAM_COLUMN_STATE,
+                        "column_3")
+                .inOrder();
+    }
+
+    @Test
+    public void testGetExistingColumns_noException() {
+        FakeTvProviderForTesting.mThrowException = false;
+        FakeTvProviderForTesting.mBundleResult = new Bundle();
+        FakeTvProviderForTesting.mBundleResult.putStringArray(
+                TvContract.EXTRA_EXISTING_COLUMN_NAMES, new String[] {"column 1", "column 2"});
+        Set<String> columns =
+                TvProviderUtils.getExistingColumns(
+                        RuntimeEnvironment.application, TvContract.Programs.CONTENT_URI);
+        assertThat(columns).containsExactly("column 1", "column 2");
+    }
+
+    @Test
+    public void testGetExistingColumns_throwsException() {
+        FakeTvProviderForTesting.mThrowException = true;
+        FakeTvProviderForTesting.mBundleResult = new Bundle();
+        // should be no exception here
+        Set<String> columns =
+                TvProviderUtils.getExistingColumns(
+                        RuntimeEnvironment.application, TvContract.Programs.CONTENT_URI);
+        assertThat(columns).isEmpty();
+    }
+
+    private static class FakeTvProviderForTesting extends FakeTvProvider {
+        private static Bundle mBundleResult;
+        private static boolean mThrowException;
+
+        @Override
+        public Bundle call(String method, String arg, Bundle extras) {
+            if (mThrowException) {
+                throw new IllegalStateException();
+            }
+            return mBundleResult;
+        }
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/util/TvTrackInfoUtilsTest.java b/tests/robotests/src/com/android/tv/util/TvTrackInfoUtilsTest.java
new file mode 100644
index 0000000..f5621de
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/util/TvTrackInfoUtilsTest.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.util;
+
+import static com.android.tv.util.TvTrackInfoUtils.getBestTrackInfo;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.assertEquals;
+
+import android.media.tv.TvTrackInfo;
+import com.android.tv.testing.ComparatorTester;
+import com.android.tv.testing.constants.ConfigConstants;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link com.android.tv.util.TvTrackInfoUtils}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class TvTrackInfoUtilsTest {
+
+    /** Tests for {@link TvTrackInfoUtils#getBestTrackInfo}. */
+    private static final String UN_MATCHED_ID = "no matching ID";
+
+    private final TvTrackInfo info1En1 = create("1", "en", 1);
+
+    private final TvTrackInfo info2En5 = create("2", "en", 5);
+
+    private final TvTrackInfo info3Fr8 = create("3", "fr", 8);
+
+    private final TvTrackInfo info4Null2 = create("4", null, 2);
+
+    private final TvTrackInfo info5Null6 = create("5", null, 6);
+
+    private TvTrackInfo create(String id, String fr, int audioChannelCount) {
+        return new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, id)
+            .setLanguage(fr)
+            .setAudioChannelCount(audioChannelCount)
+            .build();
+    }
+
+    private final List<TvTrackInfo> all =
+        Arrays.asList(info1En1, info2En5, info3Fr8, info4Null2, info5Null6);
+    private final List<TvTrackInfo> nullLanguageTracks =
+        Arrays.asList(info4Null2, info5Null6);
+
+    @Test
+    public void testGetBestTrackInfo_empty() {
+        TvTrackInfo result = getBestTrackInfo(Collections.emptyList(), UN_MATCHED_ID, "en", 1);
+        assertWithMessage("best track ").that(result).isEqualTo(null);
+    }
+
+    @Test
+    public void testGetBestTrackInfo_exactMatch() {
+        TvTrackInfo result = getBestTrackInfo(all, "1", "en", 1);
+        assertWithMessage("best track ").that(result).isEqualTo(info1En1);
+    }
+
+    @Test
+    public void testGetBestTrackInfo_langAndChannelCountMatch() {
+        TvTrackInfo result = getBestTrackInfo(all, UN_MATCHED_ID, "en", 5);
+        assertWithMessage("best track ").that(result).isEqualTo(info2En5);
+    }
+
+    @Test
+    public void testGetBestTrackInfo_languageOnlyMatch() {
+        TvTrackInfo result = getBestTrackInfo(all, UN_MATCHED_ID, "fr", 1);
+        assertWithMessage("best track ").that(result).isEqualTo(info3Fr8);
+    }
+
+    @Test
+    public void testGetBestTrackInfo_channelCountOnlyMatchWithNullLanguage() {
+        TvTrackInfo result = getBestTrackInfo(all, UN_MATCHED_ID, null, 8);
+        assertWithMessage("best track ").that(result).isEqualTo(info3Fr8);
+    }
+
+    @Test
+    public void testGetBestTrackInfo_noMatches() {
+        TvTrackInfo result = getBestTrackInfo(all, UN_MATCHED_ID, "kr", 1);
+        assertWithMessage("best track ").that(result).isEqualTo(info1En1);
+    }
+
+    @Test
+    public void testGetBestTrackInfo_noMatchesWithNullLanguage() {
+        TvTrackInfo result = getBestTrackInfo(all, UN_MATCHED_ID, null, 0);
+        assertWithMessage("best track ").that(result).isEqualTo(info1En1);
+    }
+
+    @Test
+    public void testGetBestTrackInfo_channelCountAndIdMatch() {
+        TvTrackInfo result = getBestTrackInfo(nullLanguageTracks, "5", null, 6);
+        assertWithMessage("best track ").that(result).isEqualTo(info5Null6);
+    }
+
+    @Test
+    public void testComparator() {
+        Comparator<TvTrackInfo> comparator = TvTrackInfoUtils.createComparator("1", "en", 1);
+        ComparatorTester.withoutEqualsTest(comparator)
+            // lang not match
+            .addComparableGroup(
+                create("1", "kr", 1),
+                create("2", "kr", 2),
+                create("1", "ja", 1),
+                create("1", "ch", 1))
+            // lang match not count match
+            .addComparableGroup(
+                create("2", "en", 2), create("3", "en", 3), create("1", "en", 2))
+            // lang and count match
+            .addComparableGroup(create("2", "en", 1), create("3", "en", 1))
+            // all match
+            .addComparableGroup(create("1", "en", 1), create("1", "en", 1))
+            .test();
+    }
+
+    /** Tests for {@link TvTrackInfoUtils#needToShowSampleRate}. */
+
+    private final TvTrackInfo info6En1 = create("6", "en", 1);
+
+    private final TvTrackInfo info7En0 = create("7", "en", 0);
+
+    private final TvTrackInfo info8En0 = create("8", "en", 0);
+
+    private List<TvTrackInfo> trackList;
+
+    @Test
+    public void needToShowSampleRate_false() {
+        trackList = Arrays.asList(info1En1, info2En5);
+        assertEquals(
+            false,
+            TvTrackInfoUtils.needToShowSampleRate(
+                RuntimeEnvironment.application,
+                trackList));
+    }
+
+    @Test
+    public void needToShowSampleRate_sameLanguageAndChannelCount() {
+        trackList = Arrays.asList(info1En1, info6En1);
+        assertEquals(
+            true,
+            TvTrackInfoUtils.needToShowSampleRate(
+                RuntimeEnvironment.application,
+                trackList));
+    }
+
+    @Test
+    public void needToShowSampleRate_sameLanguageNoChannelCount() {
+        trackList = Arrays.asList(info7En0, info8En0);
+        assertEquals(
+            true,
+            TvTrackInfoUtils.needToShowSampleRate(
+                RuntimeEnvironment.application,
+                trackList));
+    }
+
+    /** Tests for {@link TvTrackInfoUtils#getMultiAudioString}. */
+    private static final String TRACK_ID = "test_track_id";
+    private static final int AUDIO_SAMPLE_RATE = 48000;
+
+    @Test
+    public void testAudioTrackLanguage() {
+        assertEquals(
+            "Korean",
+            TvTrackInfoUtils.getMultiAudioString(
+                RuntimeEnvironment.application,
+                createAudioTrackInfo("kor"),
+                false));
+        assertEquals(
+            "English",
+            TvTrackInfoUtils.getMultiAudioString(
+                RuntimeEnvironment.application,
+                createAudioTrackInfo("eng"),
+                false));
+        assertEquals(
+            "Unknown language",
+            TvTrackInfoUtils.getMultiAudioString(
+                RuntimeEnvironment.application,
+                createAudioTrackInfo(null),
+                false));
+        assertEquals(
+            "Unknown language",
+            TvTrackInfoUtils.getMultiAudioString(
+                RuntimeEnvironment.application,
+                createAudioTrackInfo(""),
+                false));
+        assertEquals(
+            "abc",
+            TvTrackInfoUtils.getMultiAudioString(
+                RuntimeEnvironment.application,
+                createAudioTrackInfo("abc"),
+                false));
+    }
+
+    @Test
+    public void testAudioTrackCount() {
+        assertEquals(
+            "English",
+            TvTrackInfoUtils.getMultiAudioString(
+                RuntimeEnvironment.application,
+                createAudioTrackInfo("eng", -1),
+                false));
+        assertEquals(
+            "English",
+            TvTrackInfoUtils.getMultiAudioString(
+                RuntimeEnvironment.application,
+                createAudioTrackInfo("eng", 0),
+                false));
+        assertEquals(
+            "English (mono)",
+            TvTrackInfoUtils.getMultiAudioString(
+                RuntimeEnvironment.application,
+                createAudioTrackInfo("eng", 1),
+                false));
+        assertEquals(
+            "English (stereo)",
+            TvTrackInfoUtils.getMultiAudioString(
+                RuntimeEnvironment.application,
+                createAudioTrackInfo("eng", 2),
+                false));
+        assertEquals(
+            "English (3 channels)",
+            TvTrackInfoUtils.getMultiAudioString(
+                RuntimeEnvironment.application,
+                createAudioTrackInfo("eng", 3),
+                false));
+        assertEquals(
+            "English (4 channels)",
+            TvTrackInfoUtils.getMultiAudioString(
+                RuntimeEnvironment.application,
+                createAudioTrackInfo("eng", 4),
+                false));
+        assertEquals(
+            "English (5 channels)",
+            TvTrackInfoUtils.getMultiAudioString(
+                RuntimeEnvironment.application,
+                createAudioTrackInfo("eng", 5),
+                false));
+        assertEquals(
+            "English (5.1 surround)",
+            TvTrackInfoUtils.getMultiAudioString(
+                RuntimeEnvironment.application,
+                createAudioTrackInfo("eng", 6),
+                false));
+        assertEquals(
+            "English (7 channels)",
+            TvTrackInfoUtils.getMultiAudioString(
+                RuntimeEnvironment.application,
+                createAudioTrackInfo("eng", 7),
+                false));
+        assertEquals(
+            "English (7.1 surround)",
+            TvTrackInfoUtils.getMultiAudioString(
+                RuntimeEnvironment.application,
+                createAudioTrackInfo("eng", 8),
+                false));
+    }
+
+    @Test
+    public void testShowSampleRate() {
+        assertEquals(
+            "Korean (48kHz)",
+            TvTrackInfoUtils.getMultiAudioString(
+                RuntimeEnvironment.application,
+                createAudioTrackInfo("kor", 0),
+                true));
+        assertEquals(
+            "Korean (7.1 surround, 48kHz)",
+            TvTrackInfoUtils.getMultiAudioString(
+                RuntimeEnvironment.application,
+                createAudioTrackInfo("kor", 8),
+                true));
+    }
+
+    private static TvTrackInfo createAudioTrackInfo(String language) {
+        return createAudioTrackInfo(language, 0);
+    }
+
+    private static TvTrackInfo createAudioTrackInfo(String language, int channelCount) {
+        return new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, TRACK_ID)
+            .setLanguage(language)
+            .setAudioChannelCount(channelCount)
+            .setAudioSampleRate(AUDIO_SAMPLE_RATE)
+            .build();
+    }
+
+    /** Tests for {@link TvTrackInfoUtils#toString */
+    @Test
+    public void toString_audioWithDetails() {
+        assertEquals(
+            "TvTrackInfo{type=Audio, id=1, language=en, "
+            + "description=test, audioChannelCount=1, audioSampleRate=5}",
+            TvTrackInfoUtils.toString(
+                new TvTrackInfo
+                    .Builder(TvTrackInfo.TYPE_AUDIO, "1")
+                    .setLanguage("en")
+                    .setAudioChannelCount(1)
+                    .setDescription("test")
+                    .setAudioSampleRate(5)
+                    .build()
+            ));
+    }
+
+    @Test
+    public void toString_audioWithDefaults() {
+        assertEquals(
+            "TvTrackInfo{type=Audio, id=2, language=null, "
+                + "description=null, audioChannelCount=0, audioSampleRate=0}",
+            TvTrackInfoUtils.toString(
+                new TvTrackInfo
+                    .Builder(TvTrackInfo.TYPE_AUDIO, "2")
+                    .build()
+            ));
+    }
+
+    @Test
+    public void toString_videoWithDetails() {
+        assertEquals(
+            "TvTrackInfo{type=Video, id=3, language=en, description=test, "
+            + "videoWidth=1, videoHeight=1, videoFrameRate=1.0, videoPixelAspectRatio=2.0}",
+            TvTrackInfoUtils.toString(
+                new TvTrackInfo
+                    .Builder(TvTrackInfo.TYPE_VIDEO, "3")
+                    .setLanguage("en")
+                    .setDescription("test")
+                    .setVideoWidth(1)
+                    .setVideoHeight(1)
+                    .setVideoFrameRate(1)
+                    .setVideoPixelAspectRatio(2)
+                    .build()
+            ));
+    }
+
+    @Test
+    public void toString_videoWithDefaults() {
+        assertEquals(
+            "TvTrackInfo{type=Video, id=4, language=null, description=null, "
+                + "videoWidth=0, videoHeight=0, videoFrameRate=0.0, videoPixelAspectRatio=1.0}",
+            TvTrackInfoUtils.toString(
+                new TvTrackInfo
+                    .Builder(TvTrackInfo.TYPE_VIDEO, "4")
+                    .build()
+            ));
+    }
+
+    @Test
+    public void toString_subtitleWithDetails() {
+        assertEquals(
+            "TvTrackInfo{type=Subtitle, id=5, language=en, description=test}",
+            TvTrackInfoUtils.toString(
+                new TvTrackInfo
+                    .Builder(TvTrackInfo.TYPE_SUBTITLE, "5")
+                    .setLanguage("en")
+                    .setDescription("test")
+                    .build()
+            ));
+    }
+
+    @Test
+    public void toString_subtitleWithDefaults() {
+        assertEquals(
+            "TvTrackInfo{type=Subtitle, id=6, language=null, description=null}",
+            TvTrackInfoUtils.toString(
+                new TvTrackInfo
+                    .Builder(TvTrackInfo.TYPE_SUBTITLE, "6")
+                    .build()
+            ));
+    }
+}
diff --git a/tests/robotests/src/com/android/tv/util/UtilsTest.java b/tests/robotests/src/com/android/tv/util/UtilsTest.java
new file mode 100644
index 0000000..4e8a858
--- /dev/null
+++ b/tests/robotests/src/com/android/tv/util/UtilsTest.java
@@ -0,0 +1,492 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.text.format.DateUtils;
+import com.android.tv.testing.constants.ConfigConstants;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.TimeZone;
+import org.junit.After;
+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;
+
+/**
+ * Tests for {@link com.android.tv.util.Utils#getDurationString}.
+ *
+ * <p>This test uses deprecated flags {@link DateUtils#FORMAT_12HOUR} and {@link
+ * DateUtils#FORMAT_24HOUR} to run this test independent to system's 12/24h format.
+ */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class UtilsTest {
+    // TODO: Mock Context so we can specify current time and locale for test.
+    private Locale mLocale;
+    private static final long DATE_THIS_YEAR_2_1_MS = getFebOfThisYearInMillis(1, 0, 0);
+
+    // All possible list for a parameter to test parameter independent result.
+    private static final boolean[] PARAM_USE_SHORT_FORMAT = {false, true};
+
+    @Before
+    public void setUp() {
+        // Set locale to US
+        mLocale = Locale.getDefault();
+        Locale.setDefault(Locale.US);
+    }
+
+    @After
+    public void tearDown() {
+        // Revive system locale.
+        Locale.setDefault(mLocale);
+    }
+
+    /** Return time in millis assuming that whose year is this year and month is Jan. */
+    private static long getJanOfThisYearInMillis(int date, int hour, int minutes) {
+        return new GregorianCalendar(getThisYear(), Calendar.JANUARY, date, hour, minutes)
+                .getTimeInMillis();
+    }
+
+    private static long getJanOfThisYearInMillis(int date, int hour) {
+        return getJanOfThisYearInMillis(date, hour, 0);
+    }
+
+    /** Return time in millis assuming that whose year is this year and month is Feb. */
+    private static long getFebOfThisYearInMillis(int date, int hour, int minutes) {
+        return new GregorianCalendar(getThisYear(), Calendar.FEBRUARY, date, hour, minutes)
+                .getTimeInMillis();
+    }
+
+    private static long getFebOfThisYearInMillis(int date, int hour) {
+        return getFebOfThisYearInMillis(date, hour, 0);
+    }
+
+    private static int getThisYear() {
+        return new GregorianCalendar().get(GregorianCalendar.YEAR);
+    }
+
+    @Test
+    public void testSameDateAndTime() {
+        assertEquals(
+                "3:00 AM",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(1, 3),
+                        getFebOfThisYearInMillis(1, 3),
+                        false,
+                        DateUtils.FORMAT_12HOUR));
+        assertEquals(
+                "03:00",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(1, 3),
+                        getFebOfThisYearInMillis(1, 3),
+                        false,
+                        DateUtils.FORMAT_24HOUR));
+    }
+
+    @Test
+    public void testDurationWithinToday() {
+        assertEquals(
+                "12:00 – 3:00 AM",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(1, 3),
+                        false,
+                        DateUtils.FORMAT_12HOUR));
+        assertEquals(
+                "00:00 – 03:00",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(1, 3),
+                        false,
+                        DateUtils.FORMAT_24HOUR));
+    }
+
+    @Test
+    public void testDurationFromYesterdayToToday() {
+        assertEquals(
+                "Jan 31, 3:00 AM – Feb 1, 4:00 AM",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getJanOfThisYearInMillis(31, 3),
+                        getFebOfThisYearInMillis(1, 4),
+                        false,
+                        DateUtils.FORMAT_12HOUR));
+        assertEquals(
+                "Jan 31, 03:00 – Feb 1, 04:00",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getJanOfThisYearInMillis(31, 3),
+                        getFebOfThisYearInMillis(1, 4),
+                        false,
+                        DateUtils.FORMAT_24HOUR));
+        assertEquals(
+                "1/31, 11:30 PM – 12:30 AM",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getJanOfThisYearInMillis(31, 23, 30),
+                        getFebOfThisYearInMillis(1, 0, 30),
+                        true,
+                        DateUtils.FORMAT_12HOUR));
+        assertEquals(
+                "1/31, 23:30 – 00:30",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getJanOfThisYearInMillis(31, 23, 30),
+                        getFebOfThisYearInMillis(1, 0, 30),
+                        true,
+                        DateUtils.FORMAT_24HOUR));
+    }
+
+    @Test
+    public void testDurationFromTodayToTomorrow() {
+        assertEquals(
+                "Feb 1, 3:00 AM – Feb 2, 4:00 AM",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(1, 3),
+                        getFebOfThisYearInMillis(2, 4),
+                        false,
+                        DateUtils.FORMAT_12HOUR));
+        assertEquals(
+                "Feb 1, 03:00 – Feb 2, 04:00",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(1, 3),
+                        getFebOfThisYearInMillis(2, 4),
+                        false,
+                        DateUtils.FORMAT_24HOUR));
+        assertEquals(
+                "2/1, 3:00 AM – 2/2, 4:00 AM",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(1, 3),
+                        getFebOfThisYearInMillis(2, 4),
+                        true,
+                        DateUtils.FORMAT_12HOUR));
+        assertEquals(
+                "2/1, 03:00 – 2/2, 04:00",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(1, 3),
+                        getFebOfThisYearInMillis(2, 4),
+                        true,
+                        DateUtils.FORMAT_24HOUR));
+
+        assertEquals(
+                "Feb 1, 11:30 PM – Feb 2, 12:30 AM",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(1, 23, 30),
+                        getFebOfThisYearInMillis(2, 0, 30),
+                        false,
+                        DateUtils.FORMAT_12HOUR));
+        assertEquals(
+                "Feb 1, 23:30 – Feb 2, 00:30",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(1, 23, 30),
+                        getFebOfThisYearInMillis(2, 0, 30),
+                        false,
+                        DateUtils.FORMAT_24HOUR));
+        assertEquals(
+                "11:30 PM – 12:30 AM",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(1, 23, 30),
+                        getFebOfThisYearInMillis(2, 0, 30),
+                        true,
+                        DateUtils.FORMAT_12HOUR));
+        assertEquals(
+                "23:30 – 00:30",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(1, 23, 30),
+                        getFebOfThisYearInMillis(2, 0, 30),
+                        true,
+                        DateUtils.FORMAT_24HOUR));
+    }
+
+    @Test
+    public void testDurationWithinTomorrow() {
+        assertEquals(
+                "Feb 2, 2:00 – 4:00 AM",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(2, 2),
+                        getFebOfThisYearInMillis(2, 4),
+                        false,
+                        DateUtils.FORMAT_12HOUR));
+        assertEquals(
+                "Feb 2, 02:00 – 04:00",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(2, 2),
+                        getFebOfThisYearInMillis(2, 4),
+                        false,
+                        DateUtils.FORMAT_24HOUR));
+        assertEquals(
+                "2/2, 2:00 – 4:00 AM",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(2, 2),
+                        getFebOfThisYearInMillis(2, 4),
+                        true,
+                        DateUtils.FORMAT_12HOUR));
+        assertEquals(
+                "2/2, 02:00 – 04:00",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(2, 2),
+                        getFebOfThisYearInMillis(2, 4),
+                        true,
+                        DateUtils.FORMAT_24HOUR));
+    }
+
+    @Test
+    public void testStartOfDay() {
+        assertEquals(
+                "12:00 – 1:00 AM",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(1, 1),
+                        false,
+                        DateUtils.FORMAT_12HOUR));
+        assertEquals(
+                "00:00 – 01:00",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(1, 1),
+                        false,
+                        DateUtils.FORMAT_24HOUR));
+
+        assertEquals(
+                "Feb 2, 12:00 – 1:00 AM",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(2, 0),
+                        getFebOfThisYearInMillis(2, 1),
+                        false,
+                        DateUtils.FORMAT_12HOUR));
+        assertEquals(
+                "Feb 2, 00:00 – 01:00",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(2, 0),
+                        getFebOfThisYearInMillis(2, 1),
+                        false,
+                        DateUtils.FORMAT_24HOUR));
+        assertEquals(
+                "2/2, 12:00 – 1:00 AM",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(2, 0),
+                        getFebOfThisYearInMillis(2, 1),
+                        true,
+                        DateUtils.FORMAT_12HOUR));
+        assertEquals(
+                "2/2, 00:00 – 01:00",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(2, 0),
+                        getFebOfThisYearInMillis(2, 1),
+                        true,
+                        DateUtils.FORMAT_24HOUR));
+    }
+
+    @Test
+    public void testEndOfDay() {
+        for (boolean useShortFormat : PARAM_USE_SHORT_FORMAT) {
+            assertEquals(
+                    "11:00 PM – 12:00 AM",
+                    Utils.getDurationString(
+                            RuntimeEnvironment.application,
+                            DATE_THIS_YEAR_2_1_MS,
+                            getFebOfThisYearInMillis(1, 23),
+                            getFebOfThisYearInMillis(2, 0),
+                            useShortFormat,
+                            DateUtils.FORMAT_12HOUR));
+            assertEquals(
+                    "23:00 – 00:00",
+                    Utils.getDurationString(
+                            RuntimeEnvironment.application,
+                            DATE_THIS_YEAR_2_1_MS,
+                            getFebOfThisYearInMillis(1, 23),
+                            getFebOfThisYearInMillis(2, 0),
+                            useShortFormat,
+                            DateUtils.FORMAT_24HOUR));
+        }
+
+        assertEquals(
+                "Feb 2, 11:00 PM – 12:00 AM",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(2, 23),
+                        getFebOfThisYearInMillis(3, 0),
+                        false,
+                        DateUtils.FORMAT_12HOUR));
+        assertEquals(
+                "Feb 2, 23:00 – 00:00",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(2, 23),
+                        getFebOfThisYearInMillis(3, 0),
+                        false,
+                        DateUtils.FORMAT_24HOUR));
+        assertEquals(
+                "2/2, 11:00 PM – 12:00 AM",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(2, 23),
+                        getFebOfThisYearInMillis(3, 0),
+                        true,
+                        DateUtils.FORMAT_12HOUR));
+        assertEquals(
+                "2/2, 23:00 – 00:00",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(2, 23),
+                        getFebOfThisYearInMillis(3, 0),
+                        true,
+                        DateUtils.FORMAT_24HOUR));
+        assertEquals(
+                "2/2, 12:00 AM – 2/3, 12:00 AM",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(2, 0),
+                        getFebOfThisYearInMillis(3, 0),
+                        true,
+                        DateUtils.FORMAT_12HOUR));
+        assertEquals(
+                "2/2, 00:00 – 2/3, 00:00",
+                Utils.getDurationString(
+                        RuntimeEnvironment.application,
+                        DATE_THIS_YEAR_2_1_MS,
+                        getFebOfThisYearInMillis(2, 0),
+                        getFebOfThisYearInMillis(3, 0),
+                        true,
+                        DateUtils.FORMAT_24HOUR));
+    }
+
+    @Test
+    public void testMidnight() {
+        for (boolean useShortFormat : PARAM_USE_SHORT_FORMAT) {
+            assertEquals(
+                    "12:00 AM",
+                    Utils.getDurationString(
+                            RuntimeEnvironment.application,
+                            DATE_THIS_YEAR_2_1_MS,
+                            DATE_THIS_YEAR_2_1_MS,
+                            DATE_THIS_YEAR_2_1_MS,
+                            useShortFormat,
+                            DateUtils.FORMAT_12HOUR));
+            assertEquals(
+                    "00:00",
+                    Utils.getDurationString(
+                            RuntimeEnvironment.application,
+                            DATE_THIS_YEAR_2_1_MS,
+                            DATE_THIS_YEAR_2_1_MS,
+                            DATE_THIS_YEAR_2_1_MS,
+                            useShortFormat,
+                            DateUtils.FORMAT_24HOUR));
+        }
+    }
+
+    @Test
+    public void testIsInGivenDay() {
+        assertTrue(
+                Utils.isInGivenDay(
+                        new GregorianCalendar(2015, Calendar.JANUARY, 1).getTimeInMillis(),
+                        new GregorianCalendar(2015, Calendar.JANUARY, 1, 0, 30).getTimeInMillis()));
+    }
+
+    @Test
+    public void testIsNotInGivenDay() {
+        assertFalse(
+                Utils.isInGivenDay(
+                        new GregorianCalendar(2015, Calendar.JANUARY, 1).getTimeInMillis(),
+                        new GregorianCalendar(2015, Calendar.JANUARY, 2).getTimeInMillis()));
+    }
+
+    @Test
+    public void testIfTimeZoneApplied() {
+        TimeZone timeZone = TimeZone.getDefault();
+
+        TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
+
+        // 2015.01.01 00:00 in KST = 2014.12.31 15:00 in UTC
+        long date2015StartMs = new GregorianCalendar(2015, Calendar.JANUARY, 1).getTimeInMillis();
+
+        // 2015.01.01 10:00 in KST = 2015.01.01 01:00 in UTC
+        long date2015Start10AMMs =
+                new GregorianCalendar(2015, Calendar.JANUARY, 1, 10, 0).getTimeInMillis();
+
+        // Those two times aren't in the same day in UTC, but they are in KST.
+        assertTrue(Utils.isInGivenDay(date2015StartMs, date2015Start10AMMs));
+
+        TimeZone.setDefault(timeZone);
+    }
+
+    @Test
+    public void testIsInternalTvInputInvalidInternalInputId() {
+        String inputId = "tv.comp";
+        assertFalse(Utils.isInternalTvInput(RuntimeEnvironment.application, inputId));
+    }
+}
diff --git a/tests/unit/AndroidManifest.xml b/tests/unit/AndroidManifest.xml
index c7d2f52..5ee17de 100644
--- a/tests/unit/AndroidManifest.xml
+++ b/tests/unit/AndroidManifest.xml
@@ -18,7 +18,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.tv.tests" >
 
-    <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23" />
+    <uses-sdk android:targetSdkVersion="28" android:minSdkVersion="23" />
 
     <instrumentation
         android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java b/tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java
index 4b85eaa..052123c 100644
--- a/tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java
+++ b/tests/unit/src/com/android/tv/CurrentPositionMediatorTest.java
@@ -53,9 +53,9 @@
     public void testOnSeekRequested() {
         long seekToTimeMs = System.currentTimeMillis() - REQUEST_TIMEOUT_MS * 3;
         mMediator.onSeekRequested(seekToTimeMs);
-        assertWithMessage("Seek request time")
-                .that(mMediator.mSeekRequestTimeMs)
-                .isNotSameAs(INVALID_TIME);
+    assertWithMessage("Seek request time")
+        .that(mMediator.mSeekRequestTimeMs)
+        .isNotSameInstanceAs(INVALID_TIME);
         assertWithMessage("Current position")
                 .that(mMediator.mCurrentPositionMs)
                 .isEqualTo(seekToTimeMs);
@@ -68,15 +68,15 @@
         long newCurrentTimeMs = seekToTimeMs + REQUEST_TIMEOUT_MS;
         mMediator.onSeekRequested(seekToTimeMs);
         mMediator.onCurrentPositionChanged(newCurrentTimeMs);
-        assertWithMessage("Seek request time")
-                .that(mMediator.mSeekRequestTimeMs)
-                .isNotSameAs(INVALID_TIME);
-        assertWithMessage("Current position")
-                .that(mMediator.mCurrentPositionMs)
-                .isNotSameAs(seekToTimeMs);
-        assertWithMessage("Current position")
-                .that(mMediator.mCurrentPositionMs)
-                .isNotSameAs(newCurrentTimeMs);
+    assertWithMessage("Seek request time")
+        .that(mMediator.mSeekRequestTimeMs)
+        .isNotSameInstanceAs(INVALID_TIME);
+    assertWithMessage("Current position")
+        .that(mMediator.mCurrentPositionMs)
+        .isNotSameInstanceAs(seekToTimeMs);
+    assertWithMessage("Current position")
+        .that(mMediator.mCurrentPositionMs)
+        .isNotSameInstanceAs(newCurrentTimeMs);
     }
 
     @UiThreadTest
diff --git a/tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java b/tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java
index 71ccaf3..ecae18a 100644
--- a/tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java
+++ b/tests/unit/src/com/android/tv/data/ChannelDataManagerTest.java
@@ -56,7 +56,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Matchers;
+import org.mockito.ArgumentMatchers;
 import org.mockito.Mockito;
 
 /**
@@ -92,35 +92,29 @@
         mContentResolver = new FakeContentResolver();
         mContentResolver.addProvider(TvContract.AUTHORITY, mContentProvider);
         mListener = new TestChannelDataManagerListener();
-        getInstrumentation()
-                .runOnMainSync(
-                        new Runnable() {
-                            @Override
-                            public void run() {
-                                TvInputManagerHelper mockHelper =
-                                        Mockito.mock(TvInputManagerHelper.class);
-                                Mockito.when(mockHelper.hasTvInputInfo(Matchers.anyString()))
-                                        .thenReturn(true);
-                                Context mockContext = Mockito.mock(Context.class);
-                                Mockito.when(mockContext.getContentResolver())
-                                        .thenReturn(mContentResolver);
-                                Mockito.when(mockContext.checkSelfPermission(Matchers.anyString()))
-                                  .thenAnswer(
-                                          invocation -> {
-                                              Object[] args = invocation.getArguments();
-                                              return getTargetContext()
-                                                      .checkSelfPermission(((String) args[0]));
-                                          });
-
-                                mChannelDataManager =
-                                        new ChannelDataManager(
-                                                mockContext,
-                                                mockHelper,
-                                                AsyncTask.SERIAL_EXECUTOR,
-                                                mContentResolver);
-                                mChannelDataManager.addListener(mListener);
-                            }
+    getInstrumentation()
+        .runOnMainSync(
+            new Runnable() {
+              @Override
+              public void run() {
+                TvInputManagerHelper mockHelper = Mockito.mock(TvInputManagerHelper.class);
+                Mockito.when(mockHelper.hasTvInputInfo(ArgumentMatchers.anyString()))
+                    .thenReturn(true);
+                Context mockContext = Mockito.mock(Context.class);
+                Mockito.when(mockContext.getContentResolver()).thenReturn(mContentResolver);
+                Mockito.when(mockContext.checkSelfPermission(ArgumentMatchers.anyString()))
+                    .thenAnswer(
+                        invocation -> {
+                          Object[] args = invocation.getArguments();
+                          return getTargetContext().checkSelfPermission(((String) args[0]));
                         });
+
+                mChannelDataManager =
+                    new ChannelDataManager(
+                        mockContext, mockHelper, AsyncTask.SERIAL_EXECUTOR, mContentResolver);
+                mChannelDataManager.addListener(mListener);
+              }
+            });
     }
 
     @After
diff --git a/tests/unit/src/com/android/tv/data/ChannelImplTest.java b/tests/unit/src/com/android/tv/data/ChannelImplTest.java
index 86cfab6..5879117 100644
--- a/tests/unit/src/com/android/tv/data/ChannelImplTest.java
+++ b/tests/unit/src/com/android/tv/data/ChannelImplTest.java
@@ -34,7 +34,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Matchers;
+import org.mockito.ArgumentMatchers;
 import org.mockito.Mockito;
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
@@ -46,7 +46,7 @@
     // Used for testing TV inputs with invalid input package. This could happen when a TV input is
     // uninstalled while drawing an app link card.
     private static final String INVALID_TV_INPUT_PACKAGE_NAME = "com.android.tv.invalid_tv_input";
-    // Used for testing TV inputs defined inside of Live TV.
+    // Used for testing TV inputs defined inside of TV app.
     private static final String LIVE_CHANNELS_PACKAGE_NAME = "com.android.tv";
     // Used for testing a TV input which doesn't have its leanback launcher activity.
     private static final String NONE_LEANBACK_TV_INPUT_PACKAGE_NAME =
@@ -92,30 +92,26 @@
                                 LEANBACK_TV_INPUT_PACKAGE_NAME))
                 .thenReturn(leanbackTvInputIntent);
 
-        // Channel.getAppLinkIntent() calls initAppLinkTypeAndIntent() which calls
-        // Intent.resolveActivityInfo() which calls PackageManager.getActivityInfo().
-        Mockito.doAnswer(
-                        new Answer<ActivityInfo>() {
-                            @Override
-                            public ActivityInfo answer(InvocationOnMock invocation) {
-                                // We only check the package name, since the class name can be
-                                // changed
-                                // when an intent is changed to an uri and created from the uri.
-                                // (ex, ".className" -> "packageName.className")
-                                return mValidIntent
-                                                .getComponent()
-                                                .getPackageName()
-                                                .equals(
-                                                        ((ComponentName)
-                                                                        invocation
-                                                                                .getArguments()[0])
-                                                                .getPackageName())
-                                        ? TEST_ACTIVITY_INFO
-                                        : null;
-                            }
-                        })
-                .when(mockPackageManager)
-                .getActivityInfo(Mockito.<ComponentName>any(), Mockito.anyInt());
+    // Channel.getAppLinkIntent() calls initAppLinkTypeAndIntent() which calls
+    // Intent.resolveActivityInfo() which calls PackageManager.getActivityInfo().
+    Mockito.doAnswer(
+            new Answer<ActivityInfo>() {
+              @Override
+              public ActivityInfo answer(InvocationOnMock invocation) {
+                // We only check the package name, since the class name can be
+                // changed
+                // when an intent is changed to an uri and created from the uri.
+                // (ex, ".className" -> "packageName.className")
+                return mValidIntent
+                        .getComponent()
+                        .getPackageName()
+                        .equals(((ComponentName) invocation.getArguments()[0]).getPackageName())
+                    ? TEST_ACTIVITY_INFO
+                    : null;
+              }
+            })
+        .when(mockPackageManager)
+        .getActivityInfo(ArgumentMatchers.<ComponentName>any(), ArgumentMatchers.anyInt());
 
         mMockContext = Mockito.mock(Context.class);
         Mockito.when(mMockContext.getApplicationContext()).thenReturn(mMockContext);
@@ -253,15 +249,15 @@
     @Test
     public void testComparator() {
         TvInputManagerHelper manager = Mockito.mock(TvInputManagerHelper.class);
-        Mockito.when(manager.isPartnerInput(Matchers.anyString()))
-                .thenAnswer(
-                        new Answer<Boolean>() {
-                            @Override
-                            public Boolean answer(InvocationOnMock invocation) throws Throwable {
-                                String inputId = (String) invocation.getArguments()[0];
-                                return PARTNER_INPUT_ID.equals(inputId);
-                            }
-                        });
+    Mockito.when(manager.isPartnerInput(ArgumentMatchers.anyString()))
+        .thenAnswer(
+            new Answer<Boolean>() {
+              @Override
+              public Boolean answer(InvocationOnMock invocation) throws Throwable {
+                String inputId = (String) invocation.getArguments()[0];
+                return PARTNER_INPUT_ID.equals(inputId);
+              }
+            });
         Comparator<Channel> comparator = new TestChannelComparator(manager);
         ComparatorTester<Channel> comparatorTester = ComparatorTester.withoutEqualsTest(comparator);
         comparatorTester.addComparableGroup(
@@ -306,15 +302,15 @@
     @Test
     public void testComparatorLabel() {
         TvInputManagerHelper manager = Mockito.mock(TvInputManagerHelper.class);
-        Mockito.when(manager.isPartnerInput(Matchers.anyString()))
-                .thenAnswer(
-                        new Answer<Boolean>() {
-                            @Override
-                            public Boolean answer(InvocationOnMock invocation) throws Throwable {
-                                String inputId = (String) invocation.getArguments()[0];
-                                return PARTNER_INPUT_ID.equals(inputId);
-                            }
-                        });
+    Mockito.when(manager.isPartnerInput(ArgumentMatchers.anyString()))
+        .thenAnswer(
+            new Answer<Boolean>() {
+              @Override
+              public Boolean answer(InvocationOnMock invocation) throws Throwable {
+                String inputId = (String) invocation.getArguments()[0];
+                return PARTNER_INPUT_ID.equals(inputId);
+              }
+            });
         Comparator<Channel> comparator = new ChannelComparatorWithDescriptionAsLabel(manager);
         ComparatorTester<Channel> comparatorTester = ComparatorTester.withoutEqualsTest(comparator);
 
diff --git a/tests/unit/src/com/android/tv/features/FeaturesTest.java b/tests/unit/src/com/android/tv/features/FeaturesTest.java
deleted file mode 100644
index e35758c..0000000
--- a/tests/unit/src/com/android/tv/features/FeaturesTest.java
+++ /dev/null
@@ -1,36 +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.features;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/** Test for features. */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class FeaturesTest {
-    @Test
-    public void testPropertyFeatureKeyLength() {
-        // This forces the class to be loaded and verifies all PropertyFeature key lengths.
-        // If any keys are too long the test will fail to load.
-        assertThat(TvFeatures.TEST_FEATURE.isEnabled(null)).isFalse();
-    }
-}
diff --git a/tests/unit/src/com/android/tv/menu/MenuTest.java b/tests/unit/src/com/android/tv/menu/MenuTest.java
index e384c39..7058316 100644
--- a/tests/unit/src/com/android/tv/menu/MenuTest.java
+++ b/tests/unit/src/com/android/tv/menu/MenuTest.java
@@ -25,6 +25,7 @@
 import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
 import org.mockito.Matchers;
 import org.mockito.Mockito;
 import org.mockito.invocation.InvocationOnMock;
@@ -42,8 +43,10 @@
     public void setUp() {
         mMenuView = Mockito.mock(IMenuView.class);
         MenuRowFactory factory = Mockito.mock(MenuRowFactory.class);
-        Mockito.when(factory.createMenuRow(Mockito.any(Menu.class), Mockito.any(Class.class)))
-                .thenReturn(null);
+    Mockito.when(
+            factory.createMenuRow(
+                ArgumentMatchers.any(Menu.class), ArgumentMatchers.any(Class.class)))
+        .thenReturn(null);
         mVisibilityChangeListener = Mockito.mock(OnMenuVisibilityChangeListener.class);
         mMenu = new Menu(getTargetContext(), mMenuView, factory, mVisibilityChangeListener);
         mMenu.disableAnimationForTest();
diff --git a/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java b/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java
index 5ecbdf0..64a055a 100644
--- a/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java
+++ b/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java
@@ -24,6 +24,7 @@
 import android.text.TextUtils;
 import androidx.test.filters.MediumTest;
 import androidx.test.runner.AndroidJUnit4;
+import com.android.tv.common.flags.impl.DefaultLegacyFlags;
 import com.android.tv.testing.activities.BaseMainActivityTestCase;
 import com.android.tv.testing.constants.Constants;
 import com.android.tv.testing.testinput.ChannelState;
@@ -49,7 +50,9 @@
     @Before
     public void setUp() {
         super.setUp();
-        mTvOptionsRowAdapter = new TvOptionsRowAdapter(mActivity, Collections.emptyList());
+        mTvOptionsRowAdapter =
+                new TvOptionsRowAdapter(
+                        mActivity, Collections.emptyList(), DefaultLegacyFlags.DEFAULT);
         tuneToChannel(TvTestInputConstants.CH_1_DEFAULT_DONT_MODIFY);
         waitUntilAudioTracksHaveSize(1);
         waitUntilAudioTrackSelected(ChannelState.DEFAULT.getSelectedAudioTrackId());
@@ -73,10 +76,10 @@
         waitUntilAudioTrackSelected(Constants.EN_STEREO_AUDIO_TRACK.getId());
 
         boolean result = mTvOptionsRowAdapter.updateMultiAudioAction();
-    assertWithMessage("update Action had change").that(result).isTrue();
-    assertWithMessage("Multi Audio enabled")
-        .that(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.isEnabled())
-        .isTrue();
+        assertWithMessage("update Action had change").that(result).isTrue();
+        assertWithMessage("Multi Audio enabled")
+                .that(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.isEnabled())
+                .isTrue();
     }
 
     @Test
@@ -91,10 +94,10 @@
         waitUntilAudioTrackSelected(Constants.GENERIC_AUDIO_TRACK.getId());
 
         boolean result = mTvOptionsRowAdapter.updateMultiAudioAction();
-    assertWithMessage("update Action had change").that(result).isTrue();
-    assertWithMessage("Multi Audio enabled")
-        .that(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.isEnabled())
-        .isFalse();
+        assertWithMessage("update Action had change").that(result).isTrue();
+        assertWithMessage("Multi Audio enabled")
+                .that(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.isEnabled())
+                .isFalse();
     }
 
     @Test
@@ -110,10 +113,10 @@
         waitUntilVideoTrackSelected(data.mSelectedVideoTrackId);
 
         boolean result = mTvOptionsRowAdapter.updateMultiAudioAction();
-    assertWithMessage("update Action had change").that(result).isTrue();
-    assertWithMessage("Multi Audio enabled")
-        .that(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.isEnabled())
-        .isFalse();
+        assertWithMessage("update Action had change").that(result).isTrue();
+        assertWithMessage("Multi Audio enabled")
+                .that(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.isEnabled())
+                .isFalse();
     }
 
     private void waitUntilAudioTracksHaveSize(int expected) {
diff --git a/tests/unit/src/com/android/tv/recommendation/RecommendationUtils.java b/tests/unit/src/com/android/tv/recommendation/RecommendationUtils.java
index b929a0a..eb012f4 100644
--- a/tests/unit/src/com/android/tv/recommendation/RecommendationUtils.java
+++ b/tests/unit/src/com/android/tv/recommendation/RecommendationUtils.java
@@ -26,7 +26,7 @@
 import java.util.Random;
 import java.util.TreeMap;
 import java.util.concurrent.TimeUnit;
-import org.mockito.Matchers;
+import org.mockito.ArgumentMatchers;
 import org.mockito.Mockito;
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
@@ -57,17 +57,16 @@
                         })
                 .when(dataManager)
                 .getChannelRecords();
-        Mockito.doAnswer(
-                        new Answer<ChannelRecord>() {
-                            @Override
-                            public ChannelRecord answer(InvocationOnMock invocation)
-                                    throws Throwable {
-                                long channelId = (long) invocation.getArguments()[0];
-                                return channelRecordSortedMap.get(channelId);
-                            }
-                        })
-                .when(dataManager)
-                .getChannelRecord(Matchers.anyLong());
+    Mockito.doAnswer(
+            new Answer<ChannelRecord>() {
+              @Override
+              public ChannelRecord answer(InvocationOnMock invocation) throws Throwable {
+                long channelId = (long) invocation.getArguments()[0];
+                return channelRecordSortedMap.get(channelId);
+              }
+            })
+        .when(dataManager)
+        .getChannelRecord(ArgumentMatchers.anyLong());
         return dataManager;
     }
 
@@ -131,7 +130,7 @@
                     // Time hopping with random minutes.
                     latestWatchEndTimeMs += TimeUnit.MINUTES.toMillis(mRandom.nextInt(30) + 1);
                 }
-                long watchedDurationMs = mRandom.nextInt((int) maxWatchDurationMs) + 1;
+        long watchedDurationMs = mRandom.nextInt((int) maxWatchDurationMs) + 1L;
                 if (!addWatchLog(channelId, latestWatchEndTimeMs, watchedDurationMs)) {
                     return false;
                 }
diff --git a/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java b/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java
index d914905..6cb0e08 100644
--- a/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java
+++ b/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java
@@ -17,17 +17,22 @@
 package com.android.tv.recommendation;
 
 import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
-import com.android.tv.data.Program;
+
+import com.android.tv.data.ProgramImpl;
+import com.android.tv.data.api.Program;
 import com.android.tv.recommendation.RoutineWatchEvaluator.ProgramTime;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.util.Calendar;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
-import org.junit.Test;
-import org.junit.runner.RunWith;
 
 /** Tests for {@link RoutineWatchEvaluator}. */
 @SmallTest
@@ -121,15 +126,15 @@
     @Test
     public void testCalculateTitleMatchScore_longerMatchIsBetter() {
         String base = "foo bar baz";
-        assertThat(
-                        new ScoredItem[] {
-                            score(base, ""),
-                            score(base, "bar"),
-                            score(base, "foo bar"),
-                            score(base, "foo bar baz")
-                        })
-                .asList()
-                .isOrdered();
+    assertThat(
+            new ScoredItem[] {
+              score(base, ""),
+              score(base, "bar"),
+              score(base, "foo bar"),
+              score(base, "foo bar baz")
+            })
+        .asList()
+        .isInOrder();
     }
 
     @Test
@@ -320,7 +325,7 @@
     private Program createDummyProgram(Calendar startTime, long programDurationMs) {
         long startTimeMs = startTime.getTimeInMillis();
 
-        return new Program.Builder()
+        return new ProgramImpl.Builder()
                 .setStartTimeUtcMillis(startTimeMs)
                 .setEndTimeUtcMillis(startTimeMs + programDurationMs)
                 .build();
diff --git a/tests/unit/src/com/android/tv/util/MockTvSingletons.java b/tests/unit/src/com/android/tv/util/MockTvSingletons.java
index fd4b43c..d9ff5e7 100644
--- a/tests/unit/src/com/android/tv/util/MockTvSingletons.java
+++ b/tests/unit/src/com/android/tv/util/MockTvSingletons.java
@@ -17,16 +17,15 @@
 package com.android.tv.util;
 
 import android.content.Context;
+
 import com.android.tv.InputSessionManager;
 import com.android.tv.MainActivityWrapper;
 import com.android.tv.TvApplication;
 import com.android.tv.TvSingletons;
 import com.android.tv.analytics.Analytics;
 import com.android.tv.analytics.Tracker;
-import com.android.tv.common.experiments.ExperimentLoader;
 import com.android.tv.common.flags.impl.DefaultBackendKnobsFlags;
 import com.android.tv.common.flags.impl.DefaultCloudEpgFlags;
-import com.android.tv.common.flags.impl.DefaultConcurrentDvrPlaybackFlags;
 import com.android.tv.common.flags.impl.DefaultUiFlags;
 import com.android.tv.common.recording.RecordingStorageStatusManager;
 import com.android.tv.common.singletons.HasSingletons;
@@ -34,7 +33,6 @@
 import com.android.tv.data.ChannelDataManager;
 import com.android.tv.data.PreviewDataManager;
 import com.android.tv.data.ProgramDataManager;
-import com.android.tv.data.epg.EpgFetcher;
 import com.android.tv.data.epg.EpgReader;
 import com.android.tv.dvr.DvrDataManager;
 import com.android.tv.dvr.DvrManager;
@@ -42,11 +40,14 @@
 import com.android.tv.dvr.DvrWatchedPositionManager;
 import com.android.tv.dvr.recorder.RecordingScheduler;
 import com.android.tv.perf.PerformanceMonitor;
-import com.android.tv.testing.FakeClock;
+import com.android.tv.testing.fakes.FakeClock;
 import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
+
 import com.google.common.base.Optional;
+
+import dagger.Lazy;
+
 import java.util.concurrent.Executor;
-import javax.inject.Provider;
 
 /** Mock {@link TvSingletons} class. */
 public class MockTvSingletons implements TvSingletons, HasSingletons<TvSingletons> {
@@ -56,8 +57,6 @@
     private final DefaultBackendKnobsFlags mBackendFlags = new DefaultBackendKnobsFlags();
     private final DefaultCloudEpgFlags mCloudEpgFlags = new DefaultCloudEpgFlags();
     private final DefaultUiFlags mUiFlags = new DefaultUiFlags();
-    private final DefaultConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags =
-            new DefaultConcurrentDvrPlaybackFlags();
     private PerformanceMonitor mPerformanceMonitor;
 
     public MockTvSingletons(Context context) {
@@ -78,21 +77,11 @@
     }
 
     @Override
-    public boolean isChannelDataManagerLoadFinished() {
-        return mApp.isChannelDataManagerLoadFinished();
-    }
-
-    @Override
     public ProgramDataManager getProgramDataManager() {
         return mApp.getProgramDataManager();
     }
 
     @Override
-    public boolean isProgramDataManagerCurrentProgramsLoadFinished() {
-        return mApp.isProgramDataManagerCurrentProgramsLoadFinished();
-    }
-
-    @Override
     public PreviewDataManager getPreviewDataManager() {
         return mApp.getPreviewDataManager();
     }
@@ -148,16 +137,11 @@
     }
 
     @Override
-    public Provider<EpgReader> providesEpgReader() {
+    public Lazy<EpgReader> providesEpgReader() {
         return mApp.providesEpgReader();
     }
 
     @Override
-    public EpgFetcher getEpgFetcher() {
-        return mApp.getEpgFetcher();
-    }
-
-    @Override
     public SetupUtils getSetupUtils() {
         return mApp.getSetupUtils();
     }
@@ -168,21 +152,11 @@
     }
 
     @Override
-    public ExperimentLoader getExperimentLoader() {
-        return mApp.getExperimentLoader();
-    }
-
-    @Override
     public MainActivityWrapper getMainActivityWrapper() {
         return mApp.getMainActivityWrapper();
     }
 
     @Override
-    public com.android.tv.util.account.AccountHelper getAccountHelper() {
-        return mApp.getAccountHelper();
-    }
-
-    @Override
     public boolean isRunningInMainProcess() {
         return mApp.isRunningInMainProcess();
     }
@@ -222,11 +196,6 @@
     }
 
     @Override
-    public DefaultConcurrentDvrPlaybackFlags getConcurrentDvrPlaybackFlags() {
-        return mConcurrentDvrPlaybackFlags;
-    }
-
-    @Override
     public TvSingletons singletons() {
         return this;
     }
diff --git a/tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java b/tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java
index 7e35d76..befc425 100644
--- a/tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java
+++ b/tests/unit/src/com/android/tv/util/TvInputManagerHelperTest.java
@@ -29,6 +29,7 @@
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
 import org.mockito.Mockito;
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
@@ -210,48 +211,47 @@
 
     private TvInputManagerHelper createMockTvInputManager() {
         TvInputManagerHelper manager = Mockito.mock(TvInputManagerHelper.class);
-        Mockito.doAnswer(
-                        new Answer<Boolean>() {
-                            @Override
-                            public Boolean answer(InvocationOnMock invocation) throws Throwable {
-                                TvInputInfo info = (TvInputInfo) invocation.getArguments()[0];
-                                return TEST_INPUT_MAP.get(info.getId()).mIsPartnerInput;
-                            }
-                        })
-                .when(manager)
-                .isPartnerInput(Mockito.<TvInputInfo>any());
-        Mockito.doAnswer(
-                        new Answer<String>() {
-                            @Override
-                            public String answer(InvocationOnMock invocation) throws Throwable {
-                                TvInputInfo info = (TvInputInfo) invocation.getArguments()[0];
-                                return TEST_INPUT_MAP.get(info.getId()).mLabel;
-                            }
-                        })
-                .when(manager)
-                .loadLabel(Mockito.<TvInputInfo>any());
-        Mockito.doAnswer(
-                        new Answer<String>() {
-                            @Override
-                            public String answer(InvocationOnMock invocation) throws Throwable {
-                                TvInputInfo info = (TvInputInfo) invocation.getArguments()[0];
-                                return TEST_INPUT_MAP.get(info.getId()).mCustomLabel;
-                            }
-                        })
-                .when(manager)
-                .loadCustomLabel(Mockito.<TvInputInfo>any());
-        Mockito.doAnswer(
-                        new Answer<TvInputInfo>() {
-                            @Override
-                            public TvInputInfo answer(InvocationOnMock invocation)
-                                    throws Throwable {
-                                String inputId = (String) invocation.getArguments()[0];
-                                TvInputInfoWrapper inputWrapper = TEST_INPUT_MAP.get(inputId);
-                                return inputWrapper == null ? null : inputWrapper.mInput;
-                            }
-                        })
-                .when(manager)
-                .getTvInputInfo(Mockito.<String>any());
+    Mockito.doAnswer(
+            new Answer<Boolean>() {
+              @Override
+              public Boolean answer(InvocationOnMock invocation) throws Throwable {
+                TvInputInfo info = (TvInputInfo) invocation.getArguments()[0];
+                return TEST_INPUT_MAP.get(info.getId()).mIsPartnerInput;
+              }
+            })
+        .when(manager)
+        .isPartnerInput(ArgumentMatchers.<TvInputInfo>any());
+    Mockito.doAnswer(
+            new Answer<String>() {
+              @Override
+              public String answer(InvocationOnMock invocation) throws Throwable {
+                TvInputInfo info = (TvInputInfo) invocation.getArguments()[0];
+                return TEST_INPUT_MAP.get(info.getId()).mLabel;
+              }
+            })
+        .when(manager)
+        .loadLabel(ArgumentMatchers.<TvInputInfo>any());
+    Mockito.doAnswer(
+            new Answer<String>() {
+              @Override
+              public String answer(InvocationOnMock invocation) throws Throwable {
+                TvInputInfo info = (TvInputInfo) invocation.getArguments()[0];
+                return TEST_INPUT_MAP.get(info.getId()).mCustomLabel;
+              }
+            })
+        .when(manager)
+        .loadCustomLabel(ArgumentMatchers.<TvInputInfo>any());
+    Mockito.doAnswer(
+            new Answer<TvInputInfo>() {
+              @Override
+              public TvInputInfo answer(InvocationOnMock invocation) throws Throwable {
+                String inputId = (String) invocation.getArguments()[0];
+                TvInputInfoWrapper inputWrapper = TEST_INPUT_MAP.get(inputId);
+                return inputWrapper == null ? null : inputWrapper.mInput;
+              }
+            })
+        .when(manager)
+        .getTvInputInfo(ArgumentMatchers.<String>any());
         return manager;
     }
 
diff --git a/tests/unit/src/com/android/tv/util/images/ImageCacheTest.java b/tests/unit/src/com/android/tv/util/images/ImageCacheTest.java
index 4172213..6e96824 100644
--- a/tests/unit/src/com/android/tv/util/images/ImageCacheTest.java
+++ b/tests/unit/src/com/android/tv/util/images/ImageCacheTest.java
@@ -52,28 +52,28 @@
     public void testPutIfLarger_smaller() throws Exception {
 
         mImageCache.putIfNeeded(INFO_50);
-    assertWithMessage("before").that(mImageCache.get(KEY)).isSameAs(INFO_50);
+    assertWithMessage("before").that(mImageCache.get(KEY)).isSameInstanceAs(INFO_50);
 
         mImageCache.putIfNeeded(INFO_25);
-    assertWithMessage("after").that(mImageCache.get(KEY)).isSameAs(INFO_50);
+    assertWithMessage("after").that(mImageCache.get(KEY)).isSameInstanceAs(INFO_50);
     }
 
     @Test
     public void testPutIfLarger_larger() throws Exception {
         mImageCache.putIfNeeded(INFO_50);
-    assertWithMessage("before").that(mImageCache.get(KEY)).isSameAs(INFO_50);
+    assertWithMessage("before").that(mImageCache.get(KEY)).isSameInstanceAs(INFO_50);
 
         mImageCache.putIfNeeded(INFO_100);
-    assertWithMessage("after").that(mImageCache.get(KEY)).isSameAs(INFO_100);
+    assertWithMessage("after").that(mImageCache.get(KEY)).isSameInstanceAs(INFO_100);
     }
 
     @Test
     public void testPutIfLarger_alreadyMax() throws Exception {
 
         mImageCache.putIfNeeded(INFO_100);
-    assertWithMessage("before").that(mImageCache.get(KEY)).isSameAs(INFO_100);
+    assertWithMessage("before").that(mImageCache.get(KEY)).isSameInstanceAs(INFO_100);
 
         mImageCache.putIfNeeded(INFO_200);
-    assertWithMessage("after").that(mImageCache.get(KEY)).isSameAs(INFO_100);
+    assertWithMessage("after").that(mImageCache.get(KEY)).isSameInstanceAs(INFO_100);
     }
 }
diff --git a/tuner/Android.bp b/tuner/Android.bp
index 215a1e5..d1df99f 100644
--- a/tuner/Android.bp
+++ b/tuner/Android.bp
@@ -20,25 +20,25 @@
     sdk_version: "system_current",
     resource_dirs: ["res"],
     libs: [
-        "tv-auto-value-jar",
-        "tv-auto-factory-jar",
         "android-support-annotations",
-        "tv-error-prone-annotations-jar",
-        "tv-guava-android-jar",
-        "tv-javax-annotations-jar",
-        "jsr330",
-        "tv-lib-dagger",
-        "tv-lib-exoplayer",
-        "tv-lib-exoplayer-v2-core",
-        "live-tv-tuner-proto",
         "android-support-compat",
         "android-support-core-ui",
         "android-support-v7-palette",
         "android-support-v7-recyclerview",
-        "android-support-v17-leanback",
+        "androidx.leanback_leanback",
         "androidx.tvprovider_tvprovider",
-        "tv-lib-dagger-android",
+        "jsr330",
+        "live-tv-tuner-proto",
+        "tv-auto-value-jar",
+        "tv-auto-factory-jar",
         "tv-common",
+        "tv-error-prone-annotations-jar",
+        "tv-guava-android-jar",
+        "tv-javax-annotations-jar",
+        "tv-lib-dagger",
+        "tv-lib-exoplayer",
+        "tv-lib-exoplayer-v2-core",
+        "tv-lib-dagger-android",
     ],
     plugins: [
         "tv-auto-value",
diff --git a/tuner/AndroidManifest.xml b/tuner/AndroidManifest.xml
index fd21771..c084f3f 100644
--- a/tuner/AndroidManifest.xml
+++ b/tuner/AndroidManifest.xml
@@ -18,7 +18,7 @@
     xmlns:tools="http://schemas.android.com/tools"
     package="com.android.tv.tuner"
     android:versionCode="1">
-  <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
+  <uses-sdk android:targetSdkVersion="28" android:minSdkVersion="23"/>
   <application tools:replace="android:appComponentFactory"
       android:appComponentFactory="android.support.v4.app.CoreComponentFactory" />
 </manifest>
diff --git a/tuner/SampleDvbTuner/AndroidManifest.xml b/tuner/SampleDvbTuner/AndroidManifest.xml
index 5ad927e..c46a8a6 100755
--- a/tuner/SampleDvbTuner/AndroidManifest.xml
+++ b/tuner/SampleDvbTuner/AndroidManifest.xml
@@ -19,7 +19,7 @@
 
     <uses-sdk
         android:minSdkVersion="23"
-        android:targetSdkVersion="27" />
+        android:targetSdkVersion="28" />
 
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@@ -29,6 +29,8 @@
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" />
     <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
+    <!-- Permission to modify Recorded Program -->
+    <uses-permission android:name="com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA" />
 
     <!-- Permissions/feature for USB tuner -->
     <uses-permission android:name="android.permission.DVB_DEVICE" />
@@ -87,4 +89,4 @@
             android:process="com.android.tv.tuner" />
     </application>
 
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/tuner/SampleDvbTuner/build.gradle b/tuner/SampleDvbTuner/build.gradle
index 657a425..28ad3e4 100644
--- a/tuner/SampleDvbTuner/build.gradle
+++ b/tuner/SampleDvbTuner/build.gradle
@@ -19,48 +19,35 @@
  * Experimental gradle configuration.  This file may not be up to date.
  */
 
-buildscript {
-    repositories {
-        mavenCentral()
-        google()
-        jcenter()
-    }
-    dependencies {
-        classpath 'com.android.tools.build:gradle:3.1.4'
-        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.5'
-    }
-}
 apply plugin: 'com.android.application'
 apply plugin: 'com.google.protobuf'
-android {
-    compileSdkVersion 26
-    buildToolsVersion '28.0.2'
 
-    dexOptions {
-        preDexLibraries = false
-        additionalParameters=['--core-library']
-        javaMaxHeapSize "6g"
-    }
-    android {
-        defaultConfig {
-            resConfigs "en"
-        }
-    }
-    defaultConfig {
-        minSdkVersion 23
-        targetSdkVersion 26
-        versionCode 1
-        versionName "1.0"
-    }
-    buildTypes {
-        debug {
-            minifyEnabled false
-        }
-    }
+android {
+    compileSdkVersion 28
+    buildToolsVersion '28.0.3'
+
     compileOptions() {
         sourceCompatibility JavaVersion.VERSION_1_8
         targetCompatibility JavaVersion.VERSION_1_8
     }
+
+    defaultConfig {
+        minSdkVersion 23
+        resConfigs "en"
+        targetSdkVersion 28
+        versionCode 1
+        versionName "1.0"
+    }
+
+    buildTypes {
+        debug {
+            minifyEnabled false
+        }
+        release {
+            minifyEnabled true
+        }
+    }
+
     sourceSets {
         main {
             res.srcDirs = ['res']
@@ -70,22 +57,18 @@
     }
 }
 
-repositories {
-    mavenCentral()
-    jcenter()
-    google()
-}
-
-final String SUPPORT_LIBS_VERSION = '26.1.0'
 dependencies {
-    implementation 'com.google.android.exoplayer:exoplayer-core:2.9.0'
-    implementation 'com.google.android.exoplayer:exoplayer:r1.5.16'
-    implementation "com.android.support:palette-v7:${SUPPORT_LIBS_VERSION}"
-    implementation "com.android.support:leanback-v17:${SUPPORT_LIBS_VERSION}"
-    implementation "com.android.support:support-tv-provider:${SUPPORT_LIBS_VERSION}"
-    /*Not building with  latest one (1.6.2)*/
-    annotationProcessor 'com.google.auto.value:auto-value:1.5.4'
-    implementation 'com.google.auto.value:auto-value:1.5.4'
-    implementation project(':common')
-    implementation project(':tuner')
+    implementation      'androidx.leanback:leanback:1.1.0-alpha02'
+    implementation      'androidx.palette:palette:1.0.0'
+    implementation      'androidx.tvprovider:tvprovider:1.0.0'
+
+    annotationProcessor 'com.google.auto.value:auto-value:1.5.3'
+    implementation      'com.google.auto.value:auto-value:1.5.3'
+    implementation      'com.google.dagger:dagger:2.23'
+    implementation      'com.google.dagger:dagger-android:2.23'
+    annotationProcessor 'com.google.dagger:dagger-android-processor:2.23'
+    annotationProcessor 'com.google.dagger:dagger-compiler:2.23'
+
+    implementation      project(':common')
+    implementation      project(':tuner')
 }
diff --git a/tuner/SampleDvbTuner/robotests/javatests/com/android/tv/tuner/sample/dvb/util/SampleDvbConstantsTest.java b/tuner/SampleDvbTuner/robotests/javatests/com/android/tv/tuner/sample/dvb/util/SampleDvbConstantsTest.java
new file mode 100644
index 0000000..af2ce90
--- /dev/null
+++ b/tuner/SampleDvbTuner/robotests/javatests/com/android/tv/tuner/sample/dvb/util/SampleDvbConstantsTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.sample.dvb.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ComponentName;
+import androidx.test.core.app.ApplicationProvider;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.android.tv.tuner.sample.dvb.tvinput.SampleDvbTunerTvInputService;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link SampleDvbConstants}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class SampleDvbConstantsTest {
+
+    @Test
+    public void tunerInputId() {
+        assertThat(ComponentName.unflattenFromString(SampleDvbConstants.TUNER_INPUT_ID))
+                .isEqualTo(
+                        new ComponentName(
+                                ApplicationProvider.getApplicationContext(),
+                                SampleDvbTunerTvInputService.class));
+    }
+}
diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/AndroidManifest.xml b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/AndroidManifest.xml
index dc04228..bf7c3f7 100644
--- a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/AndroidManifest.xml
+++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/AndroidManifest.xml
@@ -36,7 +36,7 @@
     <uses-feature android:name="android.software.leanback" android:required="true" />
     <uses-feature android:name="android.software.live_tv" android:required="true" />
     <uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
-    <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
+    <uses-sdk android:targetSdkVersion="28" android:minSdkVersion="23"/>
     <application
         android:name=".app.SampleDvbTuner"
         android:icon="@mipmap/ic_launcher"
diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTuner.java b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTuner.java
index 568e3c9..c45bb27 100644
--- a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTuner.java
+++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTuner.java
@@ -18,16 +18,19 @@
 
 import android.content.ComponentName;
 import android.media.tv.TvContract;
+
 import com.android.tv.common.BaseApplication;
+import com.android.tv.common.dagger.ApplicationModule;
 import com.android.tv.common.singletons.HasSingletons;
 import com.android.tv.tuner.modules.TunerSingletonsModule;
 import com.android.tv.tuner.sample.dvb.singletons.SampleDvbSingletons;
 import com.android.tv.tuner.sample.dvb.tvinput.SampleDvbTunerTvInputService;
-import com.android.tv.tuner.tvinput.factory.TunerSessionFactory;
-import com.android.tv.tuner.tvinput.factory.TunerSessionFactoryImpl;
+
 import dagger.android.AndroidInjector;
+
 import com.android.tv.common.flags.CloudEpgFlags;
 import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+
 import javax.inject.Inject;
 
 /** The top level application for Sample DVB Tuner. */
@@ -37,7 +40,6 @@
     private String mEmbeddedInputId;
     @Inject CloudEpgFlags mCloudEpgFlags;
     @Inject ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
-    @Inject TunerSessionFactoryImpl mTunerSessionFactory;
 
     @Override
     public void onCreate() {
@@ -47,7 +49,7 @@
     @Override
     protected AndroidInjector<SampleDvbTuner> applicationInjector() {
         return DaggerSampleDvbTunerComponent.builder()
-                .sampleDvbTunerModule(new SampleDvbTunerModule(this))
+                .applicationModule(new ApplicationModule(this))
                 .tunerSingletonsModule(new TunerSingletonsModule(this))
                 .build();
     }
@@ -73,16 +75,7 @@
     }
 
     @Override
-    public ConcurrentDvrPlaybackFlags getConcurrentDvrPlaybackFlags() {
-        return mConcurrentDvrPlaybackFlags;
-    }
-
-    @Override
     public SampleDvbSingletons singletons() {
         return this;
     }
-
-    public TunerSessionFactory getTunerSessionFactory() {
-        return mTunerSessionFactory;
-    }
 }
diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTunerModule.java b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTunerModule.java
index 4da3ca9..aaaa101 100644
--- a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTunerModule.java
+++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/app/SampleDvbTunerModule.java
@@ -15,38 +15,30 @@
  */
 package com.android.tv.tuner.sample.dvb.app;
 
+import com.android.tv.common.dagger.ApplicationModule;
 import com.android.tv.common.flags.impl.DefaultFlagsModule;
 import com.android.tv.tuner.api.TunerFactory;
-import com.android.tv.tuner.builtin.BuiltInTunerHalFactory;
+import com.android.tv.tuner.dvb.DvbTunerHalFactory;
 import com.android.tv.tuner.modules.TunerModule;
 import com.android.tv.tuner.sample.dvb.setup.SampleDvbTunerSetupActivity;
 import com.android.tv.tuner.sample.dvb.tvinput.SampleDvbTunerTvInputService;
-import com.android.tv.tuner.tvinput.factory.TunerSessionFactory;
+
 import dagger.Module;
 import dagger.Provides;
 
 /** Dagger module for {@link SampleDvbTuner}. */
 @Module(
         includes = {
+            ApplicationModule.class,
             DefaultFlagsModule.class,
             SampleDvbTunerTvInputService.Module.class,
             SampleDvbTunerSetupActivity.Module.class,
             TunerModule.class,
         })
 class SampleDvbTunerModule {
-    private final SampleDvbTuner mSampleDvbTuner;
-
-    SampleDvbTunerModule(SampleDvbTuner sampleDvbTuner) {
-        mSampleDvbTuner = sampleDvbTuner;
-    }
-
-    @Provides
-    public TunerSessionFactory providesTunerSessionFactory() {
-        return mSampleDvbTuner.getTunerSessionFactory();
-    }
 
     @Provides
     TunerFactory providesTunerFactory() {
-        return BuiltInTunerHalFactory.INSTANCE;
+        return DvbTunerHalFactory.INSTANCE;
     }
 }
diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/setup/SampleDvbTunerSetupActivity.java b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/setup/SampleDvbTunerSetupActivity.java
index f9ef29c..e2b5063 100644
--- a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/setup/SampleDvbTunerSetupActivity.java
+++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/setup/SampleDvbTunerSetupActivity.java
@@ -36,6 +36,7 @@
 import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
 import com.android.tv.common.util.PostalCodeUtils;
 import com.android.tv.tuner.sample.dvb.R;
+import com.android.tv.tuner.sample.dvb.util.SampleDvbConstants;
 import com.android.tv.tuner.setup.BaseTunerSetupActivity;
 import com.android.tv.tuner.setup.ConnectionTypeFragment;
 import com.android.tv.tuner.setup.LineupFragment;
@@ -55,7 +56,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
-/** An activity that serves Live TV tuner setup process. */
+/** An activity that serves Sample DVB tuner setup process. */
 public class SampleDvbTunerSetupActivity extends BaseTunerSetupActivity {
     private static final String TAG = "SampleDvbTunerSetupActivity";
     private static final boolean DEBUG = false;
@@ -78,6 +79,10 @@
     private final Runnable cancelFetchLineupTaskRunnable = this::cancelFetchLineup;
     private String embeddedInputId;
 
+    public SampleDvbTunerSetupActivity() {
+        super(SampleDvbConstants.TUNER_INPUT_ID);
+    }
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -193,6 +198,7 @@
             case ScanFragment.ACTION_CATEGORY:
                 switch (actionId) {
                     case ScanFragment.ACTION_CANCEL:
+                        clearTunerHal();
                         getFragmentManager().popBackStack();
                         return true;
                     case ScanFragment.ACTION_FINISH:
diff --git a/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/util/SampleDvbConstants.java b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/util/SampleDvbConstants.java
new file mode 100644
index 0000000..f7b34ce
--- /dev/null
+++ b/tuner/SampleDvbTuner/src/com/android/tv/tuner/sample/dvb/util/SampleDvbConstants.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.sample.dvb.util;
+
+/** Static constants for Sample DVB Tuner */
+public final class SampleDvbConstants {
+
+    /** The Input ID for the embedded tuner in Sample DVB Tuner */
+    public static final String TUNER_INPUT_ID =
+            "com.android.tv.tuner.sample.dvb/.tvinput.SampleDvbTunerTvInputService";
+
+    private SampleDvbConstants() {}
+}
diff --git a/tuner/SampleNetworkTuner/AndroidManifest.xml b/tuner/SampleNetworkTuner/AndroidManifest.xml
index 0ec9afc..22d0fe6 100755
--- a/tuner/SampleNetworkTuner/AndroidManifest.xml
+++ b/tuner/SampleNetworkTuner/AndroidManifest.xml
@@ -19,16 +19,19 @@
 
     <uses-sdk
         android:minSdkVersion="23"
-        android:targetSdkVersion="27" />
+        android:targetSdkVersion="28" />
 
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.INTERNET"/>
     <uses-permission android:name="android.permission.READ_CONTENT_RATING_SYSTEMS" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.READ_TV_LISTINGS" />
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" />
     <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
+    <!-- Permission to modify Recorded Program -->
+    <uses-permission android:name="com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA" />
 
     <!-- Permissions/feature for USB tuner -->
     <uses-permission android:name="android.permission.DVB_DEVICE" />
@@ -87,4 +90,4 @@
             android:process="com.android.tv.tuner" />
     </application>
 
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/tuner/SampleNetworkTuner/build.gradle b/tuner/SampleNetworkTuner/build.gradle
index 657a425..28ad3e4 100644
--- a/tuner/SampleNetworkTuner/build.gradle
+++ b/tuner/SampleNetworkTuner/build.gradle
@@ -19,48 +19,35 @@
  * Experimental gradle configuration.  This file may not be up to date.
  */
 
-buildscript {
-    repositories {
-        mavenCentral()
-        google()
-        jcenter()
-    }
-    dependencies {
-        classpath 'com.android.tools.build:gradle:3.1.4'
-        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.5'
-    }
-}
 apply plugin: 'com.android.application'
 apply plugin: 'com.google.protobuf'
-android {
-    compileSdkVersion 26
-    buildToolsVersion '28.0.2'
 
-    dexOptions {
-        preDexLibraries = false
-        additionalParameters=['--core-library']
-        javaMaxHeapSize "6g"
-    }
-    android {
-        defaultConfig {
-            resConfigs "en"
-        }
-    }
-    defaultConfig {
-        minSdkVersion 23
-        targetSdkVersion 26
-        versionCode 1
-        versionName "1.0"
-    }
-    buildTypes {
-        debug {
-            minifyEnabled false
-        }
-    }
+android {
+    compileSdkVersion 28
+    buildToolsVersion '28.0.3'
+
     compileOptions() {
         sourceCompatibility JavaVersion.VERSION_1_8
         targetCompatibility JavaVersion.VERSION_1_8
     }
+
+    defaultConfig {
+        minSdkVersion 23
+        resConfigs "en"
+        targetSdkVersion 28
+        versionCode 1
+        versionName "1.0"
+    }
+
+    buildTypes {
+        debug {
+            minifyEnabled false
+        }
+        release {
+            minifyEnabled true
+        }
+    }
+
     sourceSets {
         main {
             res.srcDirs = ['res']
@@ -70,22 +57,18 @@
     }
 }
 
-repositories {
-    mavenCentral()
-    jcenter()
-    google()
-}
-
-final String SUPPORT_LIBS_VERSION = '26.1.0'
 dependencies {
-    implementation 'com.google.android.exoplayer:exoplayer-core:2.9.0'
-    implementation 'com.google.android.exoplayer:exoplayer:r1.5.16'
-    implementation "com.android.support:palette-v7:${SUPPORT_LIBS_VERSION}"
-    implementation "com.android.support:leanback-v17:${SUPPORT_LIBS_VERSION}"
-    implementation "com.android.support:support-tv-provider:${SUPPORT_LIBS_VERSION}"
-    /*Not building with  latest one (1.6.2)*/
-    annotationProcessor 'com.google.auto.value:auto-value:1.5.4'
-    implementation 'com.google.auto.value:auto-value:1.5.4'
-    implementation project(':common')
-    implementation project(':tuner')
+    implementation      'androidx.leanback:leanback:1.1.0-alpha02'
+    implementation      'androidx.palette:palette:1.0.0'
+    implementation      'androidx.tvprovider:tvprovider:1.0.0'
+
+    annotationProcessor 'com.google.auto.value:auto-value:1.5.3'
+    implementation      'com.google.auto.value:auto-value:1.5.3'
+    implementation      'com.google.dagger:dagger:2.23'
+    implementation      'com.google.dagger:dagger-android:2.23'
+    annotationProcessor 'com.google.dagger:dagger-android-processor:2.23'
+    annotationProcessor 'com.google.dagger:dagger-compiler:2.23'
+
+    implementation      project(':common')
+    implementation      project(':tuner')
 }
diff --git a/tuner/SampleNetworkTuner/robotests/javatests/com/android/tv/tuner/sample/network/util/SampleNetworkConstantsTest.java b/tuner/SampleNetworkTuner/robotests/javatests/com/android/tv/tuner/sample/network/util/SampleNetworkConstantsTest.java
new file mode 100644
index 0000000..087a1a6
--- /dev/null
+++ b/tuner/SampleNetworkTuner/robotests/javatests/com/android/tv/tuner/sample/network/util/SampleNetworkConstantsTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.sample.network.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ComponentName;
+import androidx.test.core.app.ApplicationProvider;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.android.tv.tuner.sample.network.tvinput.SampleNetworkTunerTvInputService;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link SampleNetworkConstants}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class SampleNetworkConstantsTest {
+
+    @Test
+    public void tunerInputId() {
+        assertThat(ComponentName.unflattenFromString(SampleNetworkConstants.TUNER_INPUT_ID))
+                .isEqualTo(
+                        new ComponentName(
+                                ApplicationProvider.getApplicationContext(),
+                                SampleNetworkTunerTvInputService.class));
+    }
+}
diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/AndroidManifest.xml b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/AndroidManifest.xml
index dddd8a4..ad0e751 100644
--- a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/AndroidManifest.xml
+++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/AndroidManifest.xml
@@ -36,7 +36,7 @@
     <uses-feature android:name="android.software.leanback" android:required="true" />
     <uses-feature android:name="android.software.live_tv" android:required="true" />
     <uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
-    <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
+    <uses-sdk android:targetSdkVersion="28" android:minSdkVersion="23"/>
     <application
         android:name=".app.SampleNetworkTuner"
         android:icon="@mipmap/ic_launcher"
diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTuner.java b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTuner.java
index eb5b2ad..105f560 100644
--- a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTuner.java
+++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTuner.java
@@ -18,16 +18,19 @@
 
 import android.content.ComponentName;
 import android.media.tv.TvContract;
+
 import com.android.tv.common.BaseApplication;
+import com.android.tv.common.dagger.ApplicationModule;
 import com.android.tv.common.singletons.HasSingletons;
 import com.android.tv.tuner.modules.TunerSingletonsModule;
 import com.android.tv.tuner.sample.network.singletons.SampleNetworkSingletons;
 import com.android.tv.tuner.sample.network.tvinput.SampleNetworkTunerTvInputService;
-import com.android.tv.tuner.tvinput.factory.TunerSessionFactory;
-import com.android.tv.tuner.tvinput.factory.TunerSessionFactoryImpl;
+
 import dagger.android.AndroidInjector;
+
 import com.android.tv.common.flags.CloudEpgFlags;
 import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+
 import javax.inject.Inject;
 
 /** The top level application for Sample DVB Tuner. */
@@ -37,7 +40,6 @@
     private String mEmbeddedInputId;
     @Inject CloudEpgFlags mCloudEpgFlags;
     @Inject ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
-    @Inject TunerSessionFactoryImpl mTunerSessionFactory;
 
     @Override
     public void onCreate() {
@@ -47,7 +49,7 @@
     @Override
     protected AndroidInjector<SampleNetworkTuner> applicationInjector() {
         return DaggerSampleNetworkTunerComponent.builder()
-                .sampleNetworkTunerModule(new SampleNetworkTunerModule(this))
+                .applicationModule(new ApplicationModule(this))
                 .tunerSingletonsModule(new TunerSingletonsModule(this))
                 .build();
     }
@@ -73,16 +75,7 @@
     }
 
     @Override
-    public ConcurrentDvrPlaybackFlags getConcurrentDvrPlaybackFlags() {
-        return mConcurrentDvrPlaybackFlags;
-    }
-
-    @Override
     public SampleNetworkSingletons singletons() {
         return this;
     }
-
-    public TunerSessionFactory getTunerSessionFactory() {
-        return mTunerSessionFactory;
-    }
 }
diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTunerModule.java b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTunerModule.java
index d974e20..3fa4502 100644
--- a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTunerModule.java
+++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/app/SampleNetworkTunerModule.java
@@ -15,38 +15,30 @@
  */
 package com.android.tv.tuner.sample.network.app;
 
+import com.android.tv.common.dagger.ApplicationModule;
 import com.android.tv.common.flags.impl.DefaultFlagsModule;
 import com.android.tv.tuner.api.TunerFactory;
-import com.android.tv.tuner.builtin.BuiltInTunerHalFactory;
+import com.android.tv.tuner.hdhomerun.HdHomeRunTunerHalFactory;
 import com.android.tv.tuner.modules.TunerModule;
 import com.android.tv.tuner.sample.network.setup.SampleNetworkTunerSetupActivity;
 import com.android.tv.tuner.sample.network.tvinput.SampleNetworkTunerTvInputService;
-import com.android.tv.tuner.tvinput.factory.TunerSessionFactory;
+
 import dagger.Module;
 import dagger.Provides;
 
 /** Dagger module for {@link SampleNetworkTuner}. */
 @Module(
         includes = {
+            ApplicationModule.class,
             DefaultFlagsModule.class,
             SampleNetworkTunerTvInputService.Module.class,
             SampleNetworkTunerSetupActivity.Module.class,
             TunerModule.class,
         })
 class SampleNetworkTunerModule {
-    private final SampleNetworkTuner mSampleNetworkTuner;
-
-    SampleNetworkTunerModule(SampleNetworkTuner sampleNetworkTuner) {
-        mSampleNetworkTuner = sampleNetworkTuner;
-    }
-
-    @Provides
-    public TunerSessionFactory providesTunerSessionFactory() {
-        return mSampleNetworkTuner.getTunerSessionFactory();
-    }
 
     @Provides
     TunerFactory providesTunerFactory() {
-        return BuiltInTunerHalFactory.INSTANCE;
+        return HdHomeRunTunerHalFactory.INSTANCE;
     }
 }
diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/setup/SampleNetworkTunerSetupActivity.java b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/setup/SampleNetworkTunerSetupActivity.java
index fd783c4..755e0bb 100644
--- a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/setup/SampleNetworkTunerSetupActivity.java
+++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/setup/SampleNetworkTunerSetupActivity.java
@@ -36,6 +36,7 @@
 import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
 import com.android.tv.common.util.PostalCodeUtils;
 import com.android.tv.tuner.sample.network.R;
+import com.android.tv.tuner.sample.network.util.SampleNetworkConstants;
 import com.android.tv.tuner.setup.BaseTunerSetupActivity;
 import com.android.tv.tuner.setup.ConnectionTypeFragment;
 import com.android.tv.tuner.setup.LineupFragment;
@@ -55,7 +56,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
-/** An activity that serves Live TV tuner setup process. */
+/** An activity that serves TV app tuner setup process. */
 public class SampleNetworkTunerSetupActivity extends BaseTunerSetupActivity {
     private static final String TAG = "SampleNetworkTunerSetupActivity";
     private static final boolean DEBUG = false;
@@ -78,6 +79,10 @@
     private final Runnable cancelFetchLineupTaskRunnable = this::cancelFetchLineup;
     private String embeddedInputId;
 
+    public SampleNetworkTunerSetupActivity() {
+        super(SampleNetworkConstants.TUNER_INPUT_ID);
+    }
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
diff --git a/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/util/SampleNetworkConstants.java b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/util/SampleNetworkConstants.java
new file mode 100644
index 0000000..d0a8d25
--- /dev/null
+++ b/tuner/SampleNetworkTuner/src/com/android/tv/tuner/sample/network/util/SampleNetworkConstants.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.sample.network.util;
+
+/** Static constants for Sample Network Tuner */
+public final class SampleNetworkConstants {
+
+    /** The Input ID for the embedded tuner in Sample Network Tuner */
+    public static final String TUNER_INPUT_ID =
+            "com.android.tv.tuner.sample.network/.tvinput.SampleNetworkTunerTvInputService";
+
+    private SampleNetworkConstants() {}
+}
diff --git a/tuner/build.gradle b/tuner/build.gradle
index 0f40a29..c1001c8 100644
--- a/tuner/build.gradle
+++ b/tuner/build.gradle
@@ -21,39 +21,24 @@
 
 apply plugin: 'com.android.library'
 apply plugin: 'com.google.protobuf'
-buildscript {
-    repositories {
-        mavenCentral()
-        google()
-        jcenter()
-    }
-    dependencies {
-        classpath 'com.android.tools.build:gradle:3.1.4'
-        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.5'
-    }
-}
+
 android {
-    compileSdkVersion 26
-    buildToolsVersion '28.0.2'
+    compileSdkVersion 28
+    buildToolsVersion '28.0.3'
 
-    dexOptions {
-        preDexLibraries = false
-        additionalParameters = ['--core-library']
-        javaMaxHeapSize "6g"
-    }
-
-    android {
-        defaultConfig {
-            resConfigs "en"
-        }
+    compileOptions() {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
     }
 
     defaultConfig {
         minSdkVersion 23
-        targetSdkVersion 26
+        resConfigs "en"
+        targetSdkVersion 28
         versionCode 1
         versionName "1.0"
     }
+
     buildTypes {
         debug {
             minifyEnabled false
@@ -62,10 +47,6 @@
             minifyEnabled true
         }
     }
-    compileOptions() {
-        sourceCompatibility JavaVersion.VERSION_1_8
-        targetCompatibility JavaVersion.VERSION_1_8
-    }
 
     sourceSets {
         main {
@@ -73,43 +54,54 @@
             java.srcDirs = ['src']
             manifest.srcFile 'AndroidManifest.xml'
             proto {
-                srcDir 'proto/'
+                srcDir 'proto/src/main/proto'
             }
         }
     }
 }
 
-repositories {
-    mavenCentral()
-    google()
-    jcenter()
-}
-
-final String SUPPORT_LIBS_VERSION = '26.1.0'
 dependencies {
-    implementation 'com.google.android.exoplayer:exoplayer-core:2.9.0'
-    implementation 'com.google.android.exoplayer:exoplayer:r1.5.16'
-    implementation "com.android.support:support-tv-provider:${SUPPORT_LIBS_VERSION}"
-    implementation "com.android.support:appcompat-v7:${SUPPORT_LIBS_VERSION}"
-    implementation "com.android.support:leanback-v17:${SUPPORT_LIBS_VERSION}"
-    implementation 'com.google.guava:guava:26.0-android'
-    implementation 'com.google.protobuf.nano:protobuf-javanano:3.2.0rc2'
-    implementation project(':common')
+    implementation      'androidx.annotation:annotation:1.1.0'
+    implementation      'androidx.appcompat:appcompat:1.0.2'
+    implementation      'androidx.leanback:leanback:1.1.0-alpha02'
+    implementation      'androidx.recyclerview:recyclerview:1.0.0'
+    implementation      'androidx.recyclerview:recyclerview-selection:1.0.0'
+    implementation      'androidx.tvprovider:tvprovider:1.0.0'
+
+    implementation      'com.google.android.exoplayer:exoplayer:r1.5.16'
+    implementation      'com.google.android.exoplayer:exoplayer-core:2.10.1'
+    annotationProcessor 'com.google.auto.factory:auto-factory:1.0-beta6'
+    implementation      'com.google.auto.factory:auto-factory:1.0-beta6'
+    implementation      'com.google.dagger:dagger:2.23'
+    implementation      'com.google.dagger:dagger-android:2.23'
+    annotationProcessor 'com.google.dagger:dagger-android-processor:2.23'
+    annotationProcessor 'com.google.dagger:dagger-compiler:2.23'
+    implementation      'com.google.guava:guava:28.0-jre'
+    implementation      'com.google.protobuf:protobuf-java:3.0.0'
+
+    implementation      project(':common')
 }
 protobuf {
     // Configure the protoc executable
     protoc {
-        artifact = 'com.google.protobuf:protoc:3.1.0'
+        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
-                    javanano {
-                        option "enum_style=java"
-                    }
                 }
+                    task.plugins {
+                        javalite {}
+                    }
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/tuner/proto/Android.bp b/tuner/proto/Android.bp
index 67f35f8..d1728a6 100644
--- a/tuner/proto/Android.bp
+++ b/tuner/proto/Android.bp
@@ -19,8 +19,7 @@
     srcs: ["*.proto"],
     sdk_version: "system_current",
     proto: {
-        type: "nano",
-        output_params: ["enum_style=java"],
+        type: "lite",
         canonical_path_from_root: false,
     },
     min_sdk_version: "23",
diff --git a/tuner/proto/channel.proto b/tuner/proto/channel.proto
index ff372ad..815ffbc 100644
--- a/tuner/proto/channel.proto
+++ b/tuner/proto/channel.proto
@@ -18,18 +18,13 @@
 
 package com.android.tv.tuner.data;
 
+import "track.proto";
+
 option java_package = "com.android.tv.tuner.data";
 option java_outer_classname = "Channel";
 
-
-// AOSP_Comment_Out import "third_party/android/nanoproto/nano_descriptor.proto";
-
-import "track.proto";
-
 // Holds information about a channel used in the tuners.
 message TunerChannelProto {
-// AOSP_Comment_Out   option (proto2.nano.message_as_lite) = false;
-
   optional TunerType type = 1;
   optional string short_name = 2;
   optional string long_name = 3;
@@ -51,22 +46,24 @@
   optional int32 audio_track_index = 19;
   repeated AtscCaptionTrack caption_tracks = 20;
   optional bool has_caption_track = 21;
-  optional AtscServiceType service_type = 22 [default = SERVICE_TYPE_ATSC_DIGITAL_TELEVISION];
+  optional AtscServiceType service_type = 22
+      [default = SERVICE_TYPE_ATSC_DIGITAL_TELEVISION];
   optional bool recording_prohibited = 23;
   optional string video_format = 24;
   /**
    * The flag indicating whether this TV channel is locked or not.
-   * This is primarily used for alternative parental control to prevent unauthorized users from
-   * watching the current channel regardless of the content rating
-   * @see <a href="https://developer.android.com/reference/android/media/tv/TvContract.Channels.html#COLUMN_LOCKED">link</a>
+   * This is primarily used for alternative parental control to prevent
+   * unauthorized users from watching the current channel regardless of the
+   * content rating
+   * @see <a
+   * href="https://developer.android.com/reference/android/media/tv/TvContract.Channels.html#COLUMN_LOCKED">link</a>
    */
   optional bool locked = 25;
+  optional DeliverySystemType delivery_system_type = 26;
 }
 
 // Enum describing the types of tuner.
 enum TunerType {
-// AOSP_Comment_Out   option (proto2.nano.enum_as_lite) = false;
-
   TYPE_TUNER = 0;
   TYPE_FILE = 1;
   TYPE_NETWORK = 2;
@@ -74,8 +71,10 @@
 
 // Enum describing the types of video stream.
 enum VideoStreamType {
-// AOSP_Comment_Out   option (proto2.nano.enum_as_lite) = false;
-
+  // Default unset value.  The spec says 0 is reserved.
+  UNSET = 0x00;
+  // DEPRECATED: previously used as default or unset value
+  INVALID_STREAMTYPE = -1 [deprecated=true];
   // ISO/IEC 11172 Video (MPEG-1)
   MPEG1 = 0x01;
   // ISO/IEC 13818-2 (MPEG-2) Video
@@ -90,8 +89,6 @@
 
 // Enum describing the types of audio stream.
 enum AudioStreamType {
-// AOSP_Comment_Out   option (proto2.nano.enum_as_lite) = false;
-
   // ISO/IEC 11172 Audio (MPEG-1)
   MPEG1AUDIO = 0x03;
   // ISO/IEC 13818-3 Audio (MPEG-2)
@@ -109,8 +106,6 @@
 // Enum describing ATSC service types
 // See ATSC Code Points Registry.
 enum AtscServiceType {
-// AOSP_Comment_Out   option (proto2.nano.enum_as_lite) = false;
-
   SERVICE_TYPE_ATSC_RESERVED = 0x0;
   SERVICE_TYPE_ANALOG_TELEVISION_CHANNELS = 0x1;
   SERVICE_TYPE_ATSC_DIGITAL_TELEVISION = 0x2;
@@ -122,3 +117,15 @@
   SERVICE_TYPE_ATSC_NRT_SERVICE = 0x8;
   SERVICE_TYPE_EXTENDED_PARAMERTERIZED_SERVICE = 0x9;
 }
+
+// Enum describing the types of delivery system.
+enum DeliverySystemType {
+  // Do not reorder. Must match Tuner.java
+  DELIVERY_SYSTEM_UNDEFINED = 0;
+  DELIVERY_SYSTEM_ATSC = 1;
+  DELIVERY_SYSTEM_DVBC = 2;
+  DELIVERY_SYSTEM_DVBS = 3;
+  DELIVERY_SYSTEM_DVBS2 = 4;
+  DELIVERY_SYSTEM_DVBT = 5;
+  DELIVERY_SYSTEM_DVBT2 = 6;
+}
diff --git a/tuner/proto/track.proto b/tuner/proto/track.proto
index 11ca784..a4a20db 100644
--- a/tuner/proto/track.proto
+++ b/tuner/proto/track.proto
@@ -18,15 +18,11 @@
 
 package com.android.tv.tuner.data;
 
-// AOSP_Comment_Out import "third_party/android/nanoproto/nano_descriptor.proto";
-
 option java_package = "com.android.tv.tuner.data";
 option java_outer_classname = "Track";
 
 // Represents a AC3 audio track.
 message AtscAudioTrack {
-// AOSP_Comment_Out   option (proto2.nano.message_as_lite) = false; 
-
   optional string language = 1;
   optional AudioType audio_type = 2;
   optional int32 index = 3;
@@ -36,8 +32,6 @@
   // Enum describing the types of a audio track.
   // See ISO/IEC 138181-1:2000(e) Table 2-53.
   enum AudioType {
-// AOSP_Comment_Out     option (proto2.nano.enum_as_lite) = false;
-
     AUDIOTYPE_UNDEFINED = 0;
     AUDIOTYPE_CLEAN_EFFECTS = 1;
     AUDIOTYPE_HEARING_IMPAIRED = 2;
@@ -47,11 +41,8 @@
 
 // Represents a CEA-708 caption track.
 message AtscCaptionTrack {
-// AOSP_Comment_Out   option (proto2.nano.message_as_lite) = false;
-
   optional string language = 1;
   optional int32 service_number = 2;
   optional bool easy_reader = 3;
   optional bool wide_aspect_ratio = 4;
 }
-
diff --git a/tuner/res/layout/guided_action_editable.xml b/tuner/res/layout/guided_action_editable.xml
index 84f56f8..4d3c87f 100644
--- a/tuner/res/layout/guided_action_editable.xml
+++ b/tuner/res/layout/guided_action_editable.xml
@@ -15,13 +15,13 @@
   ~ limitations under the License.
   -->
 
-<android.support.v17.leanback.widget.GuidedActionItemContainer
+<androidx.leanback.widget.GuidedActionItemContainer
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/guidedactions_editable"
     style="?attr/guidedActionItemContainerStyle"
     android:layout_height="88dp">
 
-    <android.support.v17.leanback.widget.NonOverlappingLinearLayout
+    <androidx.leanback.widget.NonOverlappingLinearLayout
         android:id="@+id/guidedactions_item_content"
         style="?attr/guidedActionItemContentStyle" >
 
@@ -32,10 +32,10 @@
             android:textColor="@color/lb_guidedactions_item_unselected_text_color"
             android:textSize="16sp" />
 
-        <android.support.v17.leanback.widget.GuidedActionEditText
+        <androidx.leanback.widget.GuidedActionEditText
             android:id="@+id/guidedactions_item_description"
             style="?attr/guidedActionItemDescriptionStyle" />
 
-    </android.support.v17.leanback.widget.NonOverlappingLinearLayout>
+    </androidx.leanback.widget.NonOverlappingLinearLayout>
 
-</android.support.v17.leanback.widget.GuidedActionItemContainer>
\ No newline at end of file
+</androidx.leanback.widget.GuidedActionItemContainer>
\ No newline at end of file
diff --git a/tuner/res/raw/ut_euro_dvbt_all b/tuner/res/raw/ut_euro_dvbt_all
index 101ee3e..a2ff03b 100644
--- a/tuner/res/raw/ut_euro_dvbt_all
+++ b/tuner/res/raw/ut_euro_dvbt_all
@@ -1,66 +1,6 @@
 # Euro DVB-T frequencies
 # Only Germany and England frequencies are added now.
 
-# Frequencies from Germany
-T 474000000 QAM16
-T 474000000 QAM64
-T 482000000 QAM16
-T 490000000 QAM16
-T 498000000 QAM16
-T 498000000 QAM64
-T 506000000 QAM16
-T 506000000 QAM64
-T 514000000 QAM16
-T 522000000 QAM16
-T 522000000 QAM64
-T 530000000 QAM16
-T 538000000 QAM16
-T 538000000 QAM64
-T 546000000 QAM16
-T 554000000 QAM16
-T 554000000 QAM64
-T 562000000 QAM16
-T 562000000 QAM64
-T 570000000 QAM16
-T 578000000 QAM16
-T 578000000 QAM64
-T 586000000 QAM16
-T 594000000 QAM16
-T 602000000 QAM16
-T 602000000 QAM64
-T 610000000 QAM16
-T 610000000 QAM64
-T 618000000 QAM16
-T 618000000 QAM64
-T 626000000 QAM16
-T 634000000 QAM16
-T 634000000 QAM64
-T 642000000 QAM16
-T 650000000 QAM16
-T 658000000 QAM16
-T 666000000 QAM16
-T 674000000 QAM16
-T 674000000 QAM64
-T 682000000 QAM16
-T 690000000 QAM16
-T 690000000 QAM64
-T 698000000 QAM16
-T 698000000 QAM64
-T 706000000 QAM16
-T 722000000 QAM16
-T 730000000 QAM16
-T 730000000 QAM64
-T 738000000 QAM16
-T 738000000 QAM64
-T 746000000 QAM16
-T 746000000 QPSK
-T 754000000 QAM16
-T 762000000 QAM16
-T 770000000 QAM16
-T 778000000 QAM16
-T 786000000 QAM16
-
-# Frequencies from England
 T 474000000 QAM16
 T 474000000 QAM64
 T 474167000 QAM16
@@ -130,6 +70,7 @@
 T 562167000 QAM16
 T 569833000 QAM64
 T 570000000 QAM16
+T 570000000 QAM64
 T 570167000 QAM16
 T 577833000 QAM16
 T 578000000 QAM16
@@ -138,7 +79,9 @@
 T 578167000 QAM16
 T 578167000 QAM64
 T 586000000 QAM16
+T 586000000 QAM256
 T 594000000 QAM16
+T 594000000 QAM64
 T 602000000 QAM16
 T 602000000 QAM64
 T 610000000 QAM16
@@ -224,11 +167,13 @@
 T 746000000 QAM16
 T 746000000 QAM64
 T 746000000 QPSK
+T 746000000 QAM256
 T 746167000 QAM64
 T 753833000 QAM16
 T 753833000 QAM64
 T 754000000 QAM16
 T 754000000 QAM64
+T 754000000 QAM256
 T 754167000 QAM16
 T 761833000 QAM16
 T 761833000 QAM64
diff --git a/tuner/src/com/android/tv/tuner/TunerHal.java b/tuner/src/com/android/tv/tuner/TunerHal.java
index dce4f4c..3f469d6 100644
--- a/tuner/src/com/android/tv/tuner/TunerHal.java
+++ b/tuner/src/com/android/tv/tuner/TunerHal.java
@@ -36,6 +36,7 @@
     private static final int DEFAULT_QAM_TUNE_TIMEOUT_MS = 4000; // Some device takes time for
 
     @DeliverySystemType private int mDeliverySystemType;
+    @DeliverySystemType private int[] mDeliverySystemTypes;
     private boolean mIsStreaming;
     private int mFrequency;
     private String mModulation;
@@ -57,9 +58,16 @@
     }
 
     protected void getDeliverySystemTypeFromDevice() {
+        getDeliverySystemTypesFromDevice();
+    }
+
+    protected void getDeliverySystemTypesFromDevice() {
         if (mDeliverySystemType == DELIVERY_SYSTEM_UNDEFINED) {
             mDeliverySystemType = nativeGetDeliverySystemType(getDeviceId());
         }
+        if (mDeliverySystemTypes == null) {
+            mDeliverySystemTypes = nativeGetDeliverySystemTypes(getDeviceId());
+        }
     }
 
     /**
@@ -79,18 +87,34 @@
 
     protected native void nativeFinalize(long deviceId);
 
+    @Override
+    public synchronized boolean tune(
+                int frequency, @ModulationType String modulation,
+            String channelNumber) {
+        return tuneInternal(mDeliverySystemType, frequency, modulation, channelNumber);
+    }
+
+    @Override
+    public synchronized boolean tune(
+            int deliverySystemType, int frequency, @ModulationType String modulation,
+            String channelNumber) {
+        return tuneInternal(deliverySystemType, frequency, modulation, channelNumber);
+    }
+
     /**
      * Sets the tuner channel. This should be called after acquiring a tuner device.
      *
+     * @param deliverySystemType a system delivery type of the channel to tune to
      * @param frequency a frequency of the channel to tune to
      * @param modulation a modulation method of the channel to tune to
      * @param channelNumber channel number when channel number is already known. Some tuner HAL may
      *     use channelNumber instead of frequency for tune.
      * @return {@code true} if the operation was successful, {@code false} otherwise
      */
-    @Override
-    public synchronized boolean tune(
-            int frequency, @ModulationType String modulation, String channelNumber) {
+    protected boolean tuneInternal(
+        int deliverySystemType, int frequency, @ModulationType String modulation,
+        String channelNumber) {
+
         if (!isDeviceOpen()) {
             Log.e(TAG, "There's no available device");
             return false;
@@ -99,40 +123,76 @@
             nativeCloseAllPidFilters(getDeviceId());
             mIsStreaming = false;
         }
+        if (mDeliverySystemTypes != null) {
+            int i;
+            for (i = 0; i < mDeliverySystemTypes.length; i++) {
+                if (deliverySystemType == mDeliverySystemTypes[i]) {
+                    break;
+                }
+            }
+
+            if (i == mDeliverySystemTypes.length) {
+                Log.e(TAG, "Unsupported delivery system type for device");
+                return false;
+            }
+        }
 
         // When tuning to a new channel in the same frequency, there's no need to stop current tuner
         // device completely and the only thing necessary for tuning is reopening pid filters.
         if (mFrequency == frequency && Objects.equals(mModulation, modulation)) {
             addPidFilter(PID_PAT, FILTER_TYPE_OTHER);
             addPidFilter(PID_ATSC_SI_BASE, FILTER_TYPE_OTHER);
-            if (Tuner.isDvbDeliverySystem(mDeliverySystemType)) {
+            if (Tuner.isDvbDeliverySystem(deliverySystemType)) {
                 addPidFilter(PID_DVB_SDT, FILTER_TYPE_OTHER);
                 addPidFilter(PID_DVB_EIT, FILTER_TYPE_OTHER);
             }
             mIsStreaming = true;
             return true;
         }
+
         int timeout_ms =
                 modulation.equals(MODULATION_8VSB)
                         ? DEFAULT_VSB_TUNE_TIMEOUT_MS
                         : DEFAULT_QAM_TUNE_TIMEOUT_MS;
-        if (nativeTune(getDeviceId(), frequency, modulation, timeout_ms)) {
+
+        boolean tuneStatus;
+        switch(deliverySystemType) {
+            case DELIVERY_SYSTEM_UNDEFINED:
+            case DELIVERY_SYSTEM_ATSC:
+                tuneStatus = nativeTune(getDeviceId(), frequency, modulation, timeout_ms);
+                break;
+            case DELIVERY_SYSTEM_DVBT:
+            case DELIVERY_SYSTEM_DVBT2:
+                tuneStatus = nativeTune(getDeviceId(), deliverySystemType, frequency, modulation,
+                        timeout_ms);
+                break;
+            default:
+                Log.e(TAG, "Unsupported delivery system type for device");
+                return false;
+        }
+
+        if (tuneStatus == true) {
             addPidFilter(PID_PAT, FILTER_TYPE_OTHER);
             addPidFilter(PID_ATSC_SI_BASE, FILTER_TYPE_OTHER);
-            if (Tuner.isDvbDeliverySystem(mDeliverySystemType)) {
+            if (Tuner.isDvbDeliverySystem(deliverySystemType)) {
                 addPidFilter(PID_DVB_SDT, FILTER_TYPE_OTHER);
                 addPidFilter(PID_DVB_EIT, FILTER_TYPE_OTHER);
             }
             mFrequency = frequency;
             mModulation = modulation;
             mIsStreaming = true;
-            return true;
         }
-        return false;
+
+        return tuneStatus;
     }
 
     protected native boolean nativeTune(
-            long deviceId, int frequency, @ModulationType String modulation, int timeout_ms);
+            long deviceId, int frequency,
+            @ModulationType String modulation, int timeout_ms);
+
+    protected native boolean nativeTune(
+            long deviceId, int deliverySystemType, int frequency,
+            @ModulationType String modulation, int timeout_ms);
 
     /**
      * Sets a pid filter. This should be set after setting a channel.
@@ -162,6 +222,8 @@
 
     protected native int nativeGetDeliverySystemType(long deviceId);
 
+    protected native int[] nativeGetDeliverySystemTypes(long deviceId);
+
     protected native int nativeGetSignalStrength(long deviceId);
 
     /**
@@ -191,6 +253,11 @@
         return mDeliverySystemType;
     }
 
+    @Override
+    public int[] getDeliverySystemTypes() {
+        return mDeliverySystemTypes;
+    }
+
     protected native void nativeStopTune(long deviceId);
 
     /**
diff --git a/tuner/src/com/android/tv/tuner/api/ScanChannel.java b/tuner/src/com/android/tv/tuner/api/ScanChannel.java
index 56e5493..1c7a6e7 100644
--- a/tuner/src/com/android/tv/tuner/api/ScanChannel.java
+++ b/tuner/src/com/android/tv/tuner/api/ScanChannel.java
@@ -15,11 +15,15 @@
  */
 package com.android.tv.tuner.api;
 
-import com.android.tv.tuner.data.nano.Channel;
+import android.util.Log;
+import com.android.tv.tuner.data.Channel;
+
 
 /** Channel information gathered from a <em>scan</em> */
 public final class ScanChannel {
+    private static final String TAG = "ScanChannel";
     public final int type;
+    public final Channel.DeliverySystemType deliverySystemType;
     public final int frequency;
     public final String modulation;
     public final String filename;
@@ -31,25 +35,60 @@
     public final Integer radioFrequencyNumber;
 
     public static ScanChannel forTuner(
-            int frequency, String modulation, Integer radioFrequencyNumber) {
+            String deliverySystemType, int frequency, String modulation,
+            Integer radioFrequencyNumber) {
         return new ScanChannel(
-                Channel.TunerType.TYPE_TUNER, frequency, modulation, null, radioFrequencyNumber);
+                Channel.TunerType.TYPE_TUNER_VALUE, lookupDeliveryStringToInt(deliverySystemType),
+                frequency, modulation, null, radioFrequencyNumber);
     }
 
     public static ScanChannel forFile(int frequency, String filename) {
-        return new ScanChannel(Channel.TunerType.TYPE_FILE, frequency, "file:", filename, null);
+        return new ScanChannel(Channel.TunerType.TYPE_FILE_VALUE,
+                Channel.DeliverySystemType.DELIVERY_SYSTEM_UNDEFINED, frequency, "file:",
+                filename, null);
     }
 
     private ScanChannel(
             int type,
+            Channel.DeliverySystemType deliverySystemType,
             int frequency,
             String modulation,
             String filename,
             Integer radioFrequencyNumber) {
         this.type = type;
+        this.deliverySystemType = deliverySystemType;
         this.frequency = frequency;
         this.modulation = modulation;
         this.filename = filename;
         this.radioFrequencyNumber = radioFrequencyNumber;
     }
+
+    private static Channel.DeliverySystemType lookupDeliveryStringToInt(String deliverySystemType) {
+        Channel.DeliverySystemType ret;
+        switch (deliverySystemType) {
+            case "A":
+                ret = Channel.DeliverySystemType.DELIVERY_SYSTEM_ATSC;
+                break;
+            case "C":
+                ret = Channel.DeliverySystemType.DELIVERY_SYSTEM_DVBC;
+                break;
+            case "S":
+                ret = Channel.DeliverySystemType.DELIVERY_SYSTEM_DVBS;
+                break;
+            case "S2":
+                ret = Channel.DeliverySystemType.DELIVERY_SYSTEM_DVBS2;
+                break;
+            case "T":
+                ret = Channel.DeliverySystemType.DELIVERY_SYSTEM_DVBT;
+                break;
+            case "T2":
+                ret = Channel.DeliverySystemType.DELIVERY_SYSTEM_DVBT2;
+                break;
+            default:
+                Log.e(TAG, "Unknown delivery system type");
+                ret = Channel.DeliverySystemType.DELIVERY_SYSTEM_UNDEFINED;
+                break;
+        }
+        return ret;
+    }
 }
diff --git a/tuner/src/com/android/tv/tuner/api/Tuner.java b/tuner/src/com/android/tv/tuner/api/Tuner.java
index 6f7e9d9..02df3ca 100644
--- a/tuner/src/com/android/tv/tuner/api/Tuner.java
+++ b/tuner/src/com/android/tv/tuner/api/Tuner.java
@@ -28,6 +28,8 @@
     int FILTER_TYPE_VIDEO = 2;
     int FILTER_TYPE_PCR = 3;
     String MODULATION_8VSB = "8VSB";
+    String MODULATION_QAM16 = "QAM16";
+    String MODULATION_QAM64 = "QAM64";
     String MODULATION_QAM256 = "QAM256";
     int DELIVERY_SYSTEM_UNDEFINED = 0;
     int DELIVERY_SYSTEM_ATSC = 1;
@@ -40,6 +42,7 @@
     int TUNER_TYPE_USB = 2;
     int TUNER_TYPE_NETWORK = 3;
     int BUILT_IN_TUNER_TYPE_LINUX_DVB = 1;
+    int BUILT_IN_TUNER_TYPE_ARCHER = 100;
 
     /** Check a delivery system is for DVB or not. */
     static boolean isDvbDeliverySystem(@DeliverySystemType int deliverySystemType) {
@@ -66,6 +69,11 @@
 
     boolean tune(int frequency, @ModulationType String modulation, String channelNumber);
 
+    default boolean tune(@DeliverySystemType int deliverySystemType, int frequency,
+                 @ModulationType String modulation, String channelNumber) {
+      return tune(frequency, modulation, channelNumber);
+    }
+
     boolean addPidFilter(int pid, @FilterType int filterType);
 
     void stopTune();
@@ -73,6 +81,10 @@
     void setHasPendingTune(boolean hasPendingTune);
 
     int getDeliverySystemType();
+    default int[] getDeliverySystemTypes() {
+      int[] deliverySystemTypes = {DELIVERY_SYSTEM_UNDEFINED};
+      return deliverySystemTypes;
+    };
 
     int readTsStream(byte[] javaBuffer, int javaBufferSize);
 
@@ -84,7 +96,7 @@
     public @interface FilterType {}
 
     /** Modulation Type */
-    @StringDef({MODULATION_8VSB, MODULATION_QAM256})
+    @StringDef({MODULATION_8VSB, MODULATION_QAM256, MODULATION_QAM16, MODULATION_QAM64})
     @Retention(RetentionPolicy.SOURCE)
     public @interface ModulationType {}
 
@@ -108,6 +120,7 @@
 
     /** Built in tuner type */
     @IntDef({
+        BUILT_IN_TUNER_TYPE_ARCHER,
         BUILT_IN_TUNER_TYPE_LINUX_DVB
     })
     @Retention(RetentionPolicy.SOURCE)
diff --git a/tuner/src/com/android/tv/tuner/cc/CaptionLayout.java b/tuner/src/com/android/tv/tuner/cc/CaptionLayout.java
index eb9ad46..62a4e15 100644
--- a/tuner/src/com/android/tv/tuner/cc/CaptionLayout.java
+++ b/tuner/src/com/android/tv/tuner/cc/CaptionLayout.java
@@ -18,7 +18,7 @@
 
 import android.content.Context;
 import android.util.AttributeSet;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
 import com.android.tv.tuner.layout.ScaledLayout;
 
 /**
diff --git a/tuner/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java b/tuner/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java
index 4a1c7c1..75776d6 100644
--- a/tuner/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java
+++ b/tuner/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java
@@ -27,7 +27,7 @@
 import com.android.tv.tuner.data.Cea708Data.CaptionWindow;
 import com.android.tv.tuner.data.Cea708Data.CaptionWindowAttr;
 import com.android.tv.tuner.data.Cea708Parser;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
 import java.util.ArrayList;
 
 /** Decodes and renders CEA-708. */
@@ -89,7 +89,7 @@
             return;
         }
         if (DEBUG) {
-            Log.d(TAG, "Start captionTrack " + captionTrack.language);
+            Log.d(TAG, "Start captionTrack " + captionTrack.getLanguage());
         }
         reset();
         mCaptionLayout.setCaptionTrack(captionTrack);
diff --git a/tuner/src/com/android/tv/tuner/cc/CaptionWindowLayout.java b/tuner/src/com/android/tv/tuner/cc/CaptionWindowLayout.java
index 13c6ff4..8c699d5 100644
--- a/tuner/src/com/android/tv/tuner/cc/CaptionWindowLayout.java
+++ b/tuner/src/com/android/tv/tuner/cc/CaptionWindowLayout.java
@@ -459,14 +459,14 @@
     private boolean isKoreanLanguageTrack() {
         return mCaptionLayout != null
                 && mCaptionLayout.getCaptionTrack() != null
-                && mCaptionLayout.getCaptionTrack().language != null
-                && "KOR".compareToIgnoreCase(mCaptionLayout.getCaptionTrack().language) == 0;
+                && mCaptionLayout.getCaptionTrack().hasLanguage()
+                && "KOR".equalsIgnoreCase(mCaptionLayout.getCaptionTrack().getLanguage());
     }
 
     private boolean isWideAspectRatio() {
         return mCaptionLayout != null
                 && mCaptionLayout.getCaptionTrack() != null
-                && mCaptionLayout.getCaptionTrack().wideAspectRatio;
+                && mCaptionLayout.getCaptionTrack().getWideAspectRatio();
     }
 
     private void updateWidestChar() {
diff --git a/tuner/src/com/android/tv/tuner/data/Cea708Parser.java b/tuner/src/com/android/tv/tuner/data/Cea708Parser.java
index 92834b2..7a5538c 100644
--- a/tuner/src/com/android/tv/tuner/data/Cea708Parser.java
+++ b/tuner/src/com/android/tv/tuner/data/Cea708Parser.java
@@ -138,6 +138,7 @@
     private long mLastDiscoveryLaunchedMs = SystemClock.elapsedRealtime();
     private int mCommand = 0;
     private int mListenServiceNumber = 0;
+    private int mDtvCcPacketCalculatedSize = 0;
     private boolean mDtvCcPacking = false;
     private boolean mFirstServiceNumberDiscovered;
 
@@ -229,6 +230,7 @@
         mBuffer.setLength(0);
         mDiscoveredNumBytes.clear();
         mCommand = 0;
+        mDtvCcPacketCalculatedSize = 0;
         mDtvCcPacking = false;
     }
 
@@ -284,29 +286,33 @@
         for (int i = 0; i < ccPacket.ccCount; ++i) {
             boolean ccValid = (bytes[pos] & 0x04) != 0;
             int ccType = bytes[pos] & 0x03;
-
-            // The dtvcc should be considered complete:
-            // - if either ccValid is set and ccType is 3
-            // - or ccValid is clear and ccType is 2 or 3.
             if (ccValid) {
+                // The dtvcc should be considered complete:
+                // if ccType is 3 or if the packet size is reached.
                 if (ccType == CC_TYPE_DTVCC_PACKET_START) {
                     if (mDtvCcPacking) {
                         parseDtvCcPacket(mDtvCcPacket.buffer(), mDtvCcPacket.length());
                         mDtvCcPacket.clear();
+                        mDtvCcPacketCalculatedSize = 0;
                     }
                     mDtvCcPacking = true;
+                    int packetSize = bytes[pos + 1] & 0x3F; // last 6 bits
+                    if (packetSize == 0) {
+                        packetSize = DTVCC_MAX_PACKET_SIZE;
+                    }
+                    mDtvCcPacketCalculatedSize = packetSize * DTVCC_PACKET_SIZE_SCALE_FACTOR;
                     mDtvCcPacket.append(bytes[pos + 1]);
                     mDtvCcPacket.append(bytes[pos + 2]);
                 } else if (mDtvCcPacking && ccType == CC_TYPE_DTVCC_PACKET_DATA) {
                     mDtvCcPacket.append(bytes[pos + 1]);
                     mDtvCcPacket.append(bytes[pos + 2]);
                 }
-            } else {
                 if ((ccType == CC_TYPE_DTVCC_PACKET_START || ccType == CC_TYPE_DTVCC_PACKET_DATA)
-                        && mDtvCcPacking) {
+                        && mDtvCcPacking && mDtvCcPacket.length() == mDtvCcPacketCalculatedSize) {
                     mDtvCcPacking = false;
                     parseDtvCcPacket(mDtvCcPacket.buffer(), mDtvCcPacket.length());
                     mDtvCcPacket.clear();
+                    mDtvCcPacketCalculatedSize = 0;
                 }
             }
             pos += 3;
diff --git a/tuner/src/com/android/tv/tuner/data/PsiData.java b/tuner/src/com/android/tv/tuner/data/PsiData.java
index 9b7c2e2..74f1603 100644
--- a/tuner/src/com/android/tv/tuner/data/PsiData.java
+++ b/tuner/src/com/android/tv/tuner/data/PsiData.java
@@ -16,8 +16,8 @@
 
 package com.android.tv.tuner.data;
 
-import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.data.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
 import java.util.List;
 
 /** Collection of MPEG PSI table items. */
diff --git a/tuner/src/com/android/tv/tuner/data/PsipData.java b/tuner/src/com/android/tv/tuner/data/PsipData.java
index d4af093..108ce3f 100644
--- a/tuner/src/com/android/tv/tuner/data/PsipData.java
+++ b/tuner/src/com/android/tv/tuner/data/PsipData.java
@@ -20,8 +20,8 @@
 import android.text.TextUtils;
 import android.text.format.DateUtils;
 import com.android.tv.common.util.StringUtils;
-import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.data.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
 import com.android.tv.tuner.util.ConvertUtils;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -495,10 +495,10 @@
         public String toString() {
             return String.format(
                     Locale.US,
-                    "AC3 audio stream sampleRateCode: %d, bsid: %d, bitRateCode: %d, "
-                            + "surroundMode: %d, bsmod: %d, numChannels: %d, fullSvc: %s, langCod: %d, "
-                            + "langCod2: %d, mainId: %d, priority: %d, avcflags: %d, text: %s, language: %s"
-                            + ", language2: %s",
+                    "AC3 audio stream sampleRateCode: %d, bsid: %d, bitRateCode: %d, surroundMode:"
+                            + " %d, bsmod: %d, numChannels: %d, fullSvc: %s, langCod: %d, langCod2:"
+                            + " %d, mainId: %d, priority: %d, avcflags: %d, text: %s, language: %s,"
+                            + " language2: %s",
                     mSampleRateCode,
                     mBsid,
                     mBitRateCode,
@@ -832,7 +832,7 @@
             }
             ArrayList<String> languages = new ArrayList<>();
             for (AtscAudioTrack audioTrack : mAudioTracks) {
-                languages.add(audioTrack.language);
+                languages.add(audioTrack.getLanguage());
             }
             return TextUtils.join(",", languages);
         }
diff --git a/tuner/src/com/android/tv/tuner/data/SectionParser.java b/tuner/src/com/android/tv/tuner/data/SectionParser.java
index d3dba6b..3c16749 100644
--- a/tuner/src/com/android/tv/tuner/data/SectionParser.java
+++ b/tuner/src/com/android/tv/tuner/data/SectionParser.java
@@ -24,7 +24,8 @@
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.SparseArray;
-
+import com.android.tv.common.feature.Model;
+import com.android.tv.tuner.data.Channel.AtscServiceType;
 import com.android.tv.tuner.data.PsiData.PatItem;
 import com.android.tv.tuner.data.PsiData.PmtItem;
 import com.android.tv.tuner.data.PsipData.Ac3AudioDescriptor;
@@ -45,9 +46,8 @@
 import com.android.tv.tuner.data.PsipData.ShortEventDescriptor;
 import com.android.tv.tuner.data.PsipData.TsDescriptor;
 import com.android.tv.tuner.data.PsipData.VctItem;
-import com.android.tv.tuner.data.nano.Channel;
-import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.data.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
 import com.android.tv.tuner.util.ByteArrayBuffer;
 import com.android.tv.tuner.util.ConvertUtils;
 import java.io.UnsupportedEncodingException;
@@ -105,7 +105,7 @@
     private static final int RATING_REGION_US_TV = 1;
     private static final int RATING_REGION_KR_TV = 4;
 
-    // The following values are defined in the live channels app.
+    // The following values are defined in the TV app.
     // See https://developer.android.com/reference/android/media/tv/TvContentRating.html.
     private static final String RATING_DOMAIN = "com.android.tv";
     private static final String RATING_REGION_RATING_SYSTEM_US_TV = "US_TV";
@@ -916,8 +916,8 @@
                 Log.d(
                         TAG,
                         String.format(
-                                "Found channel [%s] %s - serviceType: %d tsid: 0x%x program: %d "
-                                        + "channel: %d-%d encrypted: %b hidden: %b, descriptors: %d",
+                                "Found channel [%s] %s - serviceType: %d tsid: 0x%x program: %d"
+                                    + " channel: %d-%d encrypted: %b hidden: %b, descriptors: %d",
                                 shortName,
                                 longName,
                                 serviceType,
@@ -929,14 +929,14 @@
                                 hidden,
                                 descriptors.size()));
             }
-            if (!accessControlled
-                    && !hidden
-                    && (serviceType == Channel.AtscServiceType.SERVICE_TYPE_ATSC_AUDIO
+            if ((serviceType == AtscServiceType.SERVICE_TYPE_ATSC_AUDIO_VALUE
                             || serviceType
-                                    == Channel.AtscServiceType.SERVICE_TYPE_ATSC_DIGITAL_TELEVISION
+                                    == AtscServiceType.SERVICE_TYPE_ATSC_DIGITAL_TELEVISION_VALUE
                             || serviceType
-                                    == Channel.AtscServiceType
-                                            .SERVICE_TYPE_UNASSOCIATED_SMALL_SCREEN_SERVICE)) {
+                                    == AtscServiceType
+                                            .SERVICE_TYPE_UNASSOCIATED_SMALL_SCREEN_SERVICE_VALUE)
+                    && !accessControlled
+                    && !hidden) {
                 // Hide hidden, encrypted, or unsupported ATSC service type channels
                 results.add(
                         new VctItem(
@@ -1212,16 +1212,20 @@
         for (TsDescriptor descriptor : descriptors) {
             if (descriptor instanceof Ac3AudioDescriptor) {
                 Ac3AudioDescriptor audioDescriptor = (Ac3AudioDescriptor) descriptor;
-                AtscAudioTrack audioTrack = new AtscAudioTrack();
+                String language = null;
                 if (audioDescriptor.getLanguage() != null) {
-                    audioTrack.language = audioDescriptor.getLanguage();
+                    language = audioDescriptor.getLanguage();
                 }
-                if (audioTrack.language == null) {
-                    audioTrack.language = "";
+                if (language == null) {
+                    language = "";
                 }
-                audioTrack.audioType = AtscAudioTrack.AudioType.AUDIOTYPE_UNDEFINED;
-                audioTrack.channelCount = audioDescriptor.getNumChannels();
-                audioTrack.sampleRate = audioDescriptor.getSampleRate();
+                AtscAudioTrack audioTrack =
+                        AtscAudioTrack.newBuilder()
+                                .setLanguage(language)
+                                .setAudioType(AtscAudioTrack.AudioType.AUDIOTYPE_UNDEFINED)
+                                .setChannelCount(audioDescriptor.getNumChannels())
+                                .setSampleRate(audioDescriptor.getSampleRate())
+                                .build();
                 ac3Tracks.add(audioTrack);
             }
         }
@@ -1254,26 +1258,27 @@
         }
         int size = Math.max(ac3Tracks.size(), iso639LanguageTracks.size());
         for (int i = 0; i < size; ++i) {
-            AtscAudioTrack audioTrack = null;
+            AtscAudioTrack.Builder audioTrack = null;
             if (i < ac3Tracks.size()) {
-                audioTrack = ac3Tracks.get(i);
+                audioTrack = ac3Tracks.get(i).toBuilder();
             }
             if (i < iso639LanguageTracks.size()) {
                 if (audioTrack == null) {
-                    audioTrack = iso639LanguageTracks.get(i);
+                    audioTrack = iso639LanguageTracks.get(i).toBuilder();
                 } else {
                     AtscAudioTrack iso639LanguageTrack = iso639LanguageTracks.get(i);
-                    if (audioTrack.language == null || TextUtils.equals(audioTrack.language, "")) {
-                        audioTrack.language = iso639LanguageTrack.language;
+                    if (!audioTrack.hasLanguage()
+                            || TextUtils.equals(audioTrack.getLanguage(), "")) {
+                        audioTrack.setLanguage(iso639LanguageTrack.getLanguage());
                     }
-                    audioTrack.audioType = iso639LanguageTrack.audioType;
+                    audioTrack.setAudioType(iso639LanguageTrack.getAudioType());
                 }
             }
-            String language = ISO_LANGUAGE_CODE_MAP.get(audioTrack.language);
+            String language = ISO_LANGUAGE_CODE_MAP.get(audioTrack.getLanguage());
             if (language != null) {
-                audioTrack.language = language;
+                audioTrack = audioTrack.setLanguage(language);
             }
-            tracks.add(audioTrack);
+            tracks.add(audioTrack.build());
         }
         return tracks;
     }
@@ -1592,10 +1597,16 @@
                 return null;
             }
             String language = new String(data, pos, 3);
-            int audioType = data[pos + 3] & 0xff;
-            AtscAudioTrack audioTrack = new AtscAudioTrack();
-            audioTrack.language = language;
-            audioTrack.audioType = audioType;
+            int audioTypeInt = data[pos + 3] & 0xff;
+            AtscAudioTrack.AudioType audioType = AtscAudioTrack.AudioType.forNumber(audioTypeInt);
+            if (audioType == null) {
+                audioType = AtscAudioTrack.AudioType.AUDIOTYPE_UNDEFINED;
+            }
+            AtscAudioTrack audioTrack =
+                    AtscAudioTrack.newBuilder()
+                            .setLanguage(language)
+                            .setAudioType(audioType)
+                            .build();
             audioTracks.add(audioTrack);
             pos += 4;
         }
@@ -1634,11 +1645,13 @@
             reserved[0] |= (byte) ((data[pos + 1] & 0xc0) >>> 6);
             reserved[1] = (byte) ((data[pos + 1] & 0x3f) << 2);
             pos += 2;
-            AtscCaptionTrack captionTrack = new AtscCaptionTrack();
-            captionTrack.language = language;
-            captionTrack.serviceNumber = captionServiceNumber;
-            captionTrack.easyReader = easyReader;
-            captionTrack.wideAspectRatio = wideAspectRatio;
+            AtscCaptionTrack captionTrack =
+                    AtscCaptionTrack.newBuilder()
+                            .setLanguage(language)
+                            .setServiceNumber(captionServiceNumber)
+                            .setEasyReader(easyReader)
+                            .setWideAspectRatio(wideAspectRatio)
+                            .build();
             services.add(captionTrack);
         }
         return new CaptionServiceDescriptor(services);
@@ -2075,6 +2088,11 @@
     }
 
     private static boolean checkSanity(byte[] data) {
+        // Skipping CRC checking on Archer since TS data here was modified without updating CRC
+        // value. For details, see b/28616908.
+        if (Model.ARCHER.isEnabled()) {
+            return true;
+        }
         if (data.length <= 1) {
             return false;
         }
diff --git a/tuner/src/com/android/tv/tuner/data/TunerChannel.java b/tuner/src/com/android/tv/tuner/data/TunerChannel.java
index d20c343..5872cd5 100644
--- a/tuner/src/com/android/tv/tuner/data/TunerChannel.java
+++ b/tuner/src/com/android/tv/tuner/data/TunerChannel.java
@@ -20,12 +20,10 @@
 import android.support.annotation.NonNull;
 import android.util.Log;
 import com.android.tv.common.util.StringUtils;
-import com.android.tv.tuner.data.nano.Channel;
-import com.android.tv.tuner.data.nano.Channel.TunerChannelProto;
-import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
-import com.android.tv.tuner.util.Ints;
-import com.google.protobuf.nano.MessageNano;
+import com.android.tv.tuner.data.Channel.DeliverySystemType;
+import com.android.tv.tuner.data.Channel.TunerChannelProto;
+import com.android.tv.tuner.data.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -55,7 +53,7 @@
                 "Extended Parameterized Service"
             };
     private static final String ATSC_SERVICE_TYPE_NAME_RESERVED =
-            ATSC_SERVICE_TYPE_NAMES[Channel.AtscServiceType.SERVICE_TYPE_ATSC_RESERVED];
+            ATSC_SERVICE_TYPE_NAMES[Channel.AtscServiceType.SERVICE_TYPE_ATSC_RESERVED_VALUE];
 
     public static final int INVALID_FREQUENCY = -1;
 
@@ -66,93 +64,128 @@
     public static final int INVALID_STREAMTYPE = -1;
 
     // @GuardedBy(this) Writing operations and toByteArray will be guarded. b/34197766
-    private final TunerChannelProto mProto;
+    private TunerChannelProto mProto;
 
     private TunerChannel(
-            PsipData.VctItem channel, int programNumber, List<PsiData.PmtItem> pmtItems, int type) {
-        mProto = new TunerChannelProto();
-        if (channel == null) {
-            mProto.shortName = "";
-            mProto.tsid = 0;
-            mProto.programNumber = programNumber;
-            mProto.virtualMajor = 0;
-            mProto.virtualMinor = 0;
-        } else {
-            mProto.shortName = channel.getShortName();
-            if (channel.getLongName() != null) {
-                mProto.longName = channel.getLongName();
+            PsipData.VctItem channel,
+            int programNumber,
+            List<PsiData.PmtItem> pmtItems,
+            Channel.TunerType type) {
+        String shortName = "";
+        String longName = "";
+        String description = "";
+        int tsid = 0;
+        int virtualMajor = 0;
+        int virtualMinor = 0;
+        Channel.AtscServiceType serviceType =
+                Channel.AtscServiceType.SERVICE_TYPE_ATSC_DIGITAL_TELEVISION;
+        if (channel != null) {
+            shortName = channel.getShortName();
+            tsid = channel.getChannelTsid();
+            programNumber = channel.getProgramNumber();
+            virtualMajor = channel.getMajorChannelNumber();
+            virtualMinor = channel.getMinorChannelNumber();
+            Channel.AtscServiceType chanServiceType =
+                    Channel.AtscServiceType.forNumber(channel.getServiceType());
+            if (chanServiceType != null) {
+                serviceType = chanServiceType;
             }
-            mProto.tsid = channel.getChannelTsid();
-            mProto.programNumber = channel.getProgramNumber();
-            mProto.virtualMajor = channel.getMajorChannelNumber();
-            mProto.virtualMinor = channel.getMinorChannelNumber();
-            if (channel.getDescription() != null) {
-                mProto.description = channel.getDescription();
-            }
-            mProto.serviceType = channel.getServiceType();
+            longName = (channel.getLongName() != null ? channel.getLongName() : longName);
+            description =
+                    (channel.getDescription() != null ? channel.getDescription() : description);
         }
-        initProto(pmtItems, type);
+        TunerChannelProto tunerChannelProto =
+                TunerChannelProto.newBuilder()
+                        .setShortName(shortName)
+                        .setTsid(tsid)
+                        .setProgramNumber(programNumber)
+                        .setVirtualMajor(virtualMajor)
+                        .setVirtualMinor(virtualMinor)
+                        .setServiceType(serviceType)
+                        .setLongName(longName)
+                        .setDescription(description)
+                        .build();
+        initProto(pmtItems, type, tunerChannelProto);
     }
 
-    private void initProto(List<PsiData.PmtItem> pmtItems, int type) {
-        mProto.type = type;
-        mProto.channelId = -1L;
-        mProto.frequency = INVALID_FREQUENCY;
-        mProto.videoPid = INVALID_PID;
-        mProto.videoStreamType = INVALID_STREAMTYPE;
+    private void initProto(
+            List<PsiData.PmtItem> pmtItems,
+            Channel.TunerType type,
+            TunerChannelProto tunerChannelProto) {
+        int videoPid = INVALID_PID;
+        int pcrPid = 0;
+        Channel.VideoStreamType videoStreamType = Channel.VideoStreamType.UNSET;
         List<Integer> audioPids = new ArrayList<>();
-        List<Integer> audioStreamTypes = new ArrayList<>();
+        List<Channel.AudioStreamType> audioStreamTypes = new ArrayList<>();
         for (PsiData.PmtItem pmt : pmtItems) {
             switch (pmt.getStreamType()) {
                     // MPEG ES stream video types
-                case Channel.VideoStreamType.MPEG1:
-                case Channel.VideoStreamType.MPEG2:
-                case Channel.VideoStreamType.H263:
-                case Channel.VideoStreamType.H264:
-                case Channel.VideoStreamType.H265:
-                    mProto.videoPid = pmt.getEsPid();
-                    mProto.videoStreamType = pmt.getStreamType();
+                case Channel.VideoStreamType.MPEG1_VALUE:
+                case Channel.VideoStreamType.MPEG2_VALUE:
+                case Channel.VideoStreamType.H263_VALUE:
+                case Channel.VideoStreamType.H264_VALUE:
+                case Channel.VideoStreamType.H265_VALUE:
+                    videoPid = pmt.getEsPid();
+                    videoStreamType = Channel.VideoStreamType.forNumber(pmt.getStreamType());
                     break;
 
                     // MPEG ES stream audio types
-                case Channel.AudioStreamType.MPEG1AUDIO:
-                case Channel.AudioStreamType.MPEG2AUDIO:
-                case Channel.AudioStreamType.MPEG2AACAUDIO:
-                case Channel.AudioStreamType.MPEG4LATMAACAUDIO:
-                case Channel.AudioStreamType.A52AC3AUDIO:
-                case Channel.AudioStreamType.EAC3AUDIO:
+                case Channel.AudioStreamType.MPEG1AUDIO_VALUE:
+                case Channel.AudioStreamType.MPEG2AUDIO_VALUE:
+                case Channel.AudioStreamType.MPEG2AACAUDIO_VALUE:
+                case Channel.AudioStreamType.MPEG4LATMAACAUDIO_VALUE:
+                case Channel.AudioStreamType.A52AC3AUDIO_VALUE:
+                case Channel.AudioStreamType.EAC3AUDIO_VALUE:
                     audioPids.add(pmt.getEsPid());
-                    audioStreamTypes.add(pmt.getStreamType());
+                    audioStreamTypes.add(Channel.AudioStreamType.forNumber(pmt.getStreamType()));
                     break;
 
                     // Non MPEG ES stream types
                 case 0x100: // PmtItem.ES_PID_PCR:
-                    mProto.pcrPid = pmt.getEsPid();
+                    pcrPid = pmt.getEsPid();
                     break;
                 default:
                     // fall out
             }
         }
-        mProto.audioPids = Ints.toArray(audioPids);
-        mProto.audioStreamTypes = Ints.toArray(audioStreamTypes);
-        mProto.audioTrackIndex = (audioPids.size() > 0) ? 0 : -1;
+        mProto =
+                TunerChannelProto.newBuilder(tunerChannelProto)
+                        .setType(type)
+                        .setChannelId(-1L)
+                        .setFrequency(INVALID_FREQUENCY)
+                        .setVideoPid(videoPid)
+                        .setVideoStreamType(videoStreamType)
+                        .addAllAudioPids(audioPids)
+                        .setAudioTrackIndex(audioPids.isEmpty() ? -1 : 0)
+                        .addAllAudioStreamTypes(audioStreamTypes)
+                        .setPcrPid(pcrPid)
+                        .build();
     }
 
     private TunerChannel(
-            int programNumber, int type, PsipData.SdtItem channel, List<PsiData.PmtItem> pmtItems) {
-        mProto = new TunerChannelProto();
-        mProto.tsid = 0;
-        mProto.virtualMajor = 0;
-        mProto.virtualMinor = 0;
-        if (channel == null) {
-            mProto.shortName = "";
-            mProto.programNumber = programNumber;
-        } else {
-            mProto.shortName = channel.getServiceName();
-            mProto.programNumber = channel.getServiceId();
-            mProto.serviceType = channel.getServiceType();
+            int programNumber,
+            Channel.TunerType type,
+            PsipData.SdtItem channel,
+            List<PsiData.PmtItem> pmtItems) {
+        String shortName = "";
+        Channel.AtscServiceType serviceType =
+                Channel.AtscServiceType.SERVICE_TYPE_ATSC_DIGITAL_TELEVISION;
+        if (channel != null) {
+            shortName = channel.getServiceName();
+            programNumber = channel.getServiceId();
+            Channel.AtscServiceType chanServiceType =
+                    Channel.AtscServiceType.forNumber(channel.getServiceType());
+            if (chanServiceType != null) {
+                serviceType = chanServiceType;
+            }
         }
-        initProto(pmtItems, type);
+        TunerChannelProto tunerChannelProto =
+                TunerChannelProto.newBuilder()
+                        .setShortName(shortName)
+                        .setProgramNumber(programNumber)
+                        .setServiceType(serviceType)
+                        .build();
+        initProto(pmtItems, type, tunerChannelProto);
     }
 
     /** Initialize tuner channel with VCT items and PMT items. */
@@ -233,23 +266,23 @@
     }
 
     public String getName() {
-        return (!mProto.shortName.isEmpty()) ? mProto.shortName : mProto.longName;
+        return !mProto.getShortName().isEmpty() ? mProto.getShortName() : mProto.getLongName();
     }
 
     public String getShortName() {
-        return mProto.shortName;
+        return mProto.getShortName();
     }
 
     public int getProgramNumber() {
-        return mProto.programNumber;
+        return mProto.getProgramNumber();
     }
 
     public int getServiceType() {
-        return mProto.serviceType;
+        return mProto.getServiceType().getNumber();
     }
 
     public String getServiceTypeName() {
-        int serviceType = mProto.serviceType;
+        int serviceType = getServiceType();
         if (serviceType >= 0 && serviceType < ATSC_SERVICE_TYPE_NAMES.length) {
             return ATSC_SERVICE_TYPE_NAMES[serviceType];
         }
@@ -257,105 +290,129 @@
     }
 
     public int getVirtualMajor() {
-        return mProto.virtualMajor;
+        return mProto.getVirtualMajor();
     }
 
     public int getVirtualMinor() {
-        return mProto.virtualMinor;
+        return mProto.getVirtualMinor();
+    }
+
+    public DeliverySystemType getDeliverySystemType() {
+        return mProto.getDeliverySystemType();
     }
 
     public int getFrequency() {
-        return mProto.frequency;
+        return mProto.getFrequency();
     }
 
     public String getModulation() {
-        return mProto.modulation;
+        return mProto.getModulation();
     }
 
     public int getTsid() {
-        return mProto.tsid;
+        return mProto.getTsid();
     }
 
     public int getVideoPid() {
-        return mProto.videoPid;
+        return mProto.getVideoPid();
     }
 
     public synchronized void setVideoPid(int videoPid) {
-        mProto.videoPid = videoPid;
+        mProto = mProto.toBuilder().setVideoPid(videoPid).build();
     }
 
     public int getVideoStreamType() {
-        return mProto.videoStreamType;
+        return mProto.getVideoStreamType().getNumber();
     }
 
     public int getAudioPid() {
-        if (mProto.audioTrackIndex == -1) {
+        if (!mProto.hasAudioTrackIndex() || mProto.getAudioTrackIndex() == -1) {
             return INVALID_PID;
         }
-        return mProto.audioPids[mProto.audioTrackIndex];
+        return mProto.getAudioPids(mProto.getAudioTrackIndex());
     }
 
     public int getAudioStreamType() {
-        if (mProto.audioTrackIndex == -1) {
+        if (!mProto.hasAudioTrackIndex() || mProto.getAudioTrackIndex() == -1) {
             return INVALID_STREAMTYPE;
         }
-        return mProto.audioStreamTypes[mProto.audioTrackIndex];
+        return mProto.getAudioStreamTypes(mProto.getAudioTrackIndex()).getNumber();
     }
 
     public List<Integer> getAudioPids() {
-        return Ints.asList(mProto.audioPids);
+        return mProto.getAudioPidsList();
     }
 
     public synchronized void setAudioPids(List<Integer> audioPids) {
-        mProto.audioPids = Ints.toArray(audioPids);
+        mProto = mProto.toBuilder().clearAudioPids().addAllAudioPids(audioPids).build();
     }
 
     public List<Integer> getAudioStreamTypes() {
-        return Ints.asList(mProto.audioStreamTypes);
+        List<Channel.AudioStreamType> audioStreamTypes = mProto.getAudioStreamTypesList();
+        List<Integer> audioStreamTypesValues = new ArrayList<>(audioStreamTypes.size());
+
+        for (Channel.AudioStreamType audioStreamType : audioStreamTypes) {
+            audioStreamTypesValues.add(audioStreamType.getNumber());
+        }
+        return audioStreamTypesValues;
     }
 
-    public synchronized void setAudioStreamTypes(List<Integer> audioStreamTypes) {
-        mProto.audioStreamTypes = Ints.toArray(audioStreamTypes);
+    public synchronized void setAudioStreamTypes(List<Integer> audioStreamTypesValues) {
+        List<Channel.AudioStreamType> audioStreamTypes =
+                new ArrayList<>(audioStreamTypesValues.size());
+
+        for (Integer audioStreamTypesValue : audioStreamTypesValues) {
+            audioStreamTypes.add(Channel.AudioStreamType.forNumber(audioStreamTypesValue));
+        }
+        mProto =
+                mProto.toBuilder()
+                        .clearAudioStreamTypes()
+                        .addAllAudioStreamTypes(audioStreamTypes)
+                        .build();
     }
 
     public int getPcrPid() {
-        return mProto.pcrPid;
+        return mProto.getPcrPid();
     }
 
-    public int getType() {
-        return mProto.type;
+    public Channel.TunerType getType() {
+        return mProto.getType();
     }
 
     public synchronized void setFilepath(String filepath) {
-        mProto.filepath = filepath == null ? "" : filepath;
+        mProto = mProto.toBuilder().setFilepath(filepath == null ? "" : filepath).build();
     }
 
     public String getFilepath() {
-        return mProto.filepath;
+        return mProto.getFilepath();
     }
 
     public synchronized void setVirtualMajor(int virtualMajor) {
-        mProto.virtualMajor = virtualMajor;
+        mProto = mProto.toBuilder().setVirtualMajor(virtualMajor).build();
     }
 
     public synchronized void setVirtualMinor(int virtualMinor) {
-        mProto.virtualMinor = virtualMinor;
+        mProto = mProto.toBuilder().setVirtualMinor(virtualMinor).build();
     }
 
     public synchronized void setShortName(String shortName) {
-        mProto.shortName = shortName == null ? "" : shortName;
+        mProto = mProto.toBuilder().setShortName(shortName == null ? "" : shortName).build();
+    }
+
+    public synchronized void setDeliverySystemType(DeliverySystemType deliverySystemType) {
+        mProto = mProto.toBuilder().setDeliverySystemType(deliverySystemType).build();
     }
 
     public synchronized void setFrequency(int frequency) {
-        mProto.frequency = frequency;
+        mProto = mProto.toBuilder().setFrequency(frequency).build();
     }
 
     public synchronized void setModulation(String modulation) {
-        mProto.modulation = modulation == null ? "" : modulation;
+        mProto = mProto.toBuilder().setModulation(modulation == null ? "" : modulation).build();
     }
 
     public boolean hasVideo() {
-        return mProto.videoPid != INVALID_PID;
+        return mProto.hasVideoPid() && mProto.getVideoPid() != INVALID_PID;
     }
 
     public boolean hasAudio() {
@@ -363,11 +420,11 @@
     }
 
     public long getChannelId() {
-        return mProto.channelId;
+        return mProto.getChannelId();
     }
 
     public synchronized void setChannelId(long channelId) {
-        mProto.channelId = channelId;
+        mProto = mProto.toBuilder().setChannelId(channelId).build();
     }
 
     /**
@@ -379,11 +436,11 @@
      *     href="https://developer.android.com/reference/android/media/tv/TvContract.Channels.html#COLUMN_LOCKED">link</a>
      */
     public boolean isLocked() {
-        return mProto.locked;
+        return mProto.getLocked();
     }
 
     public synchronized void setLocked(boolean locked) {
-        mProto.locked = locked;
+        mProto = mProto.toBuilder().setLocked(locked).build();
     }
 
     public String getDisplayNumber() {
@@ -391,92 +448,91 @@
     }
 
     public String getDisplayNumber(boolean ignoreZeroMinorNumber) {
-        if (mProto.virtualMajor != 0 && (mProto.virtualMinor != 0 || !ignoreZeroMinorNumber)) {
+        if (getVirtualMajor() != 0 && (getVirtualMinor() != 0 || !ignoreZeroMinorNumber)) {
             return String.format(
-                    "%d%c%d", mProto.virtualMajor, CHANNEL_NUMBER_SEPARATOR, mProto.virtualMinor);
-        } else if (mProto.virtualMajor != 0) {
-            return Integer.toString(mProto.virtualMajor);
+                    "%d%c%d", getVirtualMajor(), CHANNEL_NUMBER_SEPARATOR, getVirtualMinor());
+        } else if (getVirtualMajor() != 0) {
+            return Integer.toString(getVirtualMajor());
         } else {
-            return Integer.toString(mProto.programNumber);
+            return Integer.toString(getProgramNumber());
         }
     }
 
     public String getDescription() {
-        return mProto.description;
+        return mProto.getDescription();
     }
 
     @Override
     public synchronized void setHasCaptionTrack() {
-        mProto.hasCaptionTrack = true;
+        mProto = mProto.toBuilder().setHasCaptionTrack(true).build();
     }
 
     @Override
     public boolean hasCaptionTrack() {
-        return mProto.hasCaptionTrack;
+        return mProto.getHasCaptionTrack();
     }
 
     @Override
     public List<AtscAudioTrack> getAudioTracks() {
-        return Collections.unmodifiableList(Arrays.asList(mProto.audioTracks));
+        return mProto.getAudioTracksList();
     }
 
     public synchronized void setAudioTracks(List<AtscAudioTrack> audioTracks) {
-        mProto.audioTracks = audioTracks.toArray(new AtscAudioTrack[audioTracks.size()]);
+        mProto = mProto.toBuilder().clearAudioTracks().addAllAudioTracks(audioTracks).build();
     }
 
     @Override
     public List<AtscCaptionTrack> getCaptionTracks() {
-        return Collections.unmodifiableList(Arrays.asList(mProto.captionTracks));
+        return mProto.getCaptionTracksList();
     }
 
     public synchronized void setCaptionTracks(List<AtscCaptionTrack> captionTracks) {
-        mProto.captionTracks = captionTracks.toArray(new AtscCaptionTrack[captionTracks.size()]);
+        mProto = mProto.toBuilder().clearCaptionTracks().addAllCaptionTracks(captionTracks).build();
     }
 
     public synchronized void selectAudioTrack(int index) {
-        if (0 <= index && index < mProto.audioPids.length) {
-            mProto.audioTrackIndex = index;
-        } else {
-            mProto.audioTrackIndex = -1;
+        if (index < 0 || index >= mProto.getAudioPidsCount()) {
+            index = -1;
         }
+        mProto = mProto.toBuilder().setAudioTrackIndex(index).build();
     }
 
     public synchronized void setRecordingProhibited(boolean recordingProhibited) {
-        mProto.recordingProhibited = recordingProhibited;
+        mProto = mProto.toBuilder().setRecordingProhibited(recordingProhibited).build();
     }
 
     public boolean isRecordingProhibited() {
-        return mProto.recordingProhibited;
+        return mProto.getRecordingProhibited();
     }
 
     public synchronized void setVideoFormat(String videoFormat) {
-        mProto.videoFormat = videoFormat == null ? "" : videoFormat;
+        mProto = mProto.toBuilder().setVideoFormat(videoFormat == null ? "" : videoFormat).build();
     }
 
     public String getVideoFormat() {
-        return mProto.videoFormat;
+        return mProto.getVideoFormat();
     }
 
     @Override
     public String toString() {
-        switch (mProto.type) {
-            case Channel.TunerType.TYPE_FILE:
+        switch (getType()) {
+            case TYPE_FILE:
                 return String.format(
                         "{%d-%d %s} Filepath: %s, ProgramNumber %d",
-                        mProto.virtualMajor,
-                        mProto.virtualMinor,
-                        mProto.shortName,
-                        mProto.filepath,
-                        mProto.programNumber);
+                        getVirtualMajor(),
+                        getVirtualMinor(),
+                        getShortName(),
+                        getFilepath(),
+                        getProgramNumber());
                 // case Channel.TunerType.TYPE_TUNER:
             default:
                 return String.format(
                         "{%d-%d %s} Frequency: %d, ProgramNumber %d",
-                        mProto.virtualMajor,
-                        mProto.virtualMinor,
-                        mProto.shortName,
-                        mProto.frequency,
-                        mProto.programNumber);
+                        getVirtualMajor(),
+                        getVirtualMinor(),
+                        getShortName(),
+                        getFrequency(),
+                        getProgramNumber());
         }
     }
 
@@ -495,6 +551,9 @@
         if (ret != 0) {
             return ret;
         }
+        if (getDeliverySystemType() != channel.getDeliverySystemType()) {
+            return 1;
+        }
         // For FileTsStreamer, file paths should be compared.
         return StringUtils.compare(getFilepath(), channel.getFilepath());
     }
@@ -509,20 +568,21 @@
 
     @Override
     public int hashCode() {
-        return Objects.hash(getFrequency(), getProgramNumber(), getName(), getFilepath());
+        return Objects.hash(getDeliverySystemType(), getFrequency(), getProgramNumber(), getName(),
+                getFilepath());
     }
 
     // Serialization
     public synchronized byte[] toByteArray() {
         try {
-            return MessageNano.toByteArray(mProto);
+            return mProto.toByteArray();
         } catch (Exception e) {
             // Retry toByteArray. b/34197766
             Log.w(
                     TAG,
                     "TunerChannel or its variables are modified in multiple thread without lock",
                     e);
-            return MessageNano.toByteArray(mProto);
+            return mProto.toByteArray();
         }
     }
 
diff --git a/tuner/src/com/android/tv/tuner/DvbDeviceAccessor.java b/tuner/src/com/android/tv/tuner/dvb/DvbDeviceAccessor.java
similarity index 99%
rename from tuner/src/com/android/tv/tuner/DvbDeviceAccessor.java
rename to tuner/src/com/android/tv/tuner/dvb/DvbDeviceAccessor.java
index 217433d..8be27c9 100644
--- a/tuner/src/com/android/tv/tuner/DvbDeviceAccessor.java
+++ b/tuner/src/com/android/tv/tuner/dvb/DvbDeviceAccessor.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.tv.tuner;
+package com.android.tv.tuner.dvb;
 
 import android.content.Context;
 import android.media.tv.TvInputManager;
diff --git a/tuner/src/com/android/tv/tuner/DvbTunerHal.java b/tuner/src/com/android/tv/tuner/dvb/DvbTunerHal.java
similarity index 97%
rename from tuner/src/com/android/tv/tuner/DvbTunerHal.java
rename to tuner/src/com/android/tv/tuner/dvb/DvbTunerHal.java
index c802ebb..7f68e37 100644
--- a/tuner/src/com/android/tv/tuner/DvbTunerHal.java
+++ b/tuner/src/com/android/tv/tuner/dvb/DvbTunerHal.java
@@ -14,13 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.tv.tuner;
+package com.android.tv.tuner.dvb;
 
 import android.content.Context;
 import android.os.ParcelFileDescriptor;
 import android.util.Log;
 import com.android.tv.common.compat.TvInputConstantCompat;
-import com.android.tv.tuner.DvbDeviceAccessor.DvbDeviceInfoWrapper;
+import com.android.tv.tuner.TunerHal;
+import com.android.tv.tuner.dvb.DvbDeviceAccessor.DvbDeviceInfoWrapper;
 import java.util.List;
 import java.util.SortedSet;
 import java.util.TreeSet;
diff --git a/tuner/src/com/android/tv/tuner/builtin/BuiltInTunerHalFactory.java b/tuner/src/com/android/tv/tuner/dvb/DvbTunerHalFactory.java
similarity index 60%
rename from tuner/src/com/android/tv/tuner/builtin/BuiltInTunerHalFactory.java
rename to tuner/src/com/android/tv/tuner/dvb/DvbTunerHalFactory.java
index 9a0be74..24d7e1f 100644
--- a/tuner/src/com/android/tv/tuner/builtin/BuiltInTunerHalFactory.java
+++ b/tuner/src/com/android/tv/tuner/dvb/DvbTunerHalFactory.java
@@ -14,39 +14,29 @@
  * limitations under the License.
  */
 
-package com.android.tv.tuner.builtin;
+package com.android.tv.tuner.dvb;
 
 import android.content.Context;
 import android.support.annotation.WorkerThread;
 import android.util.Log;
 import android.util.Pair;
-import com.android.tv.common.customization.CustomizationManager;
-import com.android.tv.common.feature.Model;
-import com.android.tv.tuner.DvbTunerHal;
+
 import com.android.tv.tuner.api.Tuner;
 import com.android.tv.tuner.api.TunerFactory;
 
-
 /** TunerHal factory that creates all built in tuner types. */
-public final class BuiltInTunerHalFactory implements TunerFactory {
-    private static final String TAG = "BuiltInTunerHalFactory";
+public final class DvbTunerHalFactory implements TunerFactory {
+    private static final String TAG = "DvbTunerHalFactory";
     private static final boolean DEBUG = false;
 
-    private Integer mBuiltInTunerType;
+    private final int mBuiltInTunerType = Tuner.BUILT_IN_TUNER_TYPE_LINUX_DVB;
 
-    public static final TunerFactory INSTANCE = new BuiltInTunerHalFactory();
+    public static final TunerFactory INSTANCE = new DvbTunerHalFactory();
 
-    private BuiltInTunerHalFactory() {}
+    private DvbTunerHalFactory() {}
 
     @Tuner.BuiltInTunerType
     private int getBuiltInTunerType(Context context) {
-        if (mBuiltInTunerType == null) {
-            mBuiltInTunerType = 0;
-            if (CustomizationManager.hasLinuxDvbBuiltInTuner(context)
-                    && DvbTunerHal.getNumberOfDevices(context) > 0) {
-                mBuiltInTunerType = Tuner.BUILT_IN_TUNER_TYPE_LINUX_DVB;
-            }
-        }
         return mBuiltInTunerType;
     }
 
@@ -80,17 +70,6 @@
     @Override
     @WorkerThread
     public Pair<Integer, Integer> getTunerTypeAndCount(Context context) {
-        if (useBuiltInTuner(context)) {
-            if (getBuiltInTunerType(context) == Tuner.BUILT_IN_TUNER_TYPE_LINUX_DVB) {
-                return new Pair<>(
-                        Tuner.TUNER_TYPE_BUILT_IN, DvbTunerHal.getNumberOfDevices(context));
-            }
-        } else {
-            int usbTunerCount = DvbTunerHal.getNumberOfDevices(context);
-            if (usbTunerCount > 0) {
-                return new Pair<>(Tuner.TUNER_TYPE_USB, usbTunerCount);
-            }
-        }
-        return new Pair<>(null, 0);
+        return Pair.create(Tuner.TUNER_TYPE_BUILT_IN, DvbTunerHal.getNumberOfDevices(context));
     }
 }
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java b/tuner/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
index e48cb03..124e04d 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
@@ -23,9 +23,9 @@
 import android.os.Looper;
 import android.os.Message;
 import android.os.SystemClock;
-import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
 import android.util.Pair;
+
 import com.android.tv.tuner.exoplayer.audio.MpegTsDefaultAudioTrackRenderer;
 import com.android.tv.tuner.exoplayer.buffer.BufferManager;
 import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener;
@@ -34,24 +34,20 @@
 import com.google.android.exoplayer.MediaFormat;
 import com.google.android.exoplayer.MediaFormatHolder;
 import com.google.android.exoplayer.SampleHolder;
-import com.google.android.exoplayer.upstream.DataSource;
 import com.google.android.exoplayer2.C;
 import com.google.android.exoplayer2.Format;
 import com.google.android.exoplayer2.FormatHolder;
-import com.google.android.exoplayer2.Timeline;
 import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
 import com.google.android.exoplayer2.source.ExtractorMediaSource;
-import com.google.android.exoplayer2.source.ExtractorMediaSource.EventListener;
 import com.google.android.exoplayer2.source.MediaPeriod;
 import com.google.android.exoplayer2.source.MediaSource;
 import com.google.android.exoplayer2.source.SampleStream;
 import com.google.android.exoplayer2.source.TrackGroupArray;
 import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
 import com.google.android.exoplayer2.trackselection.TrackSelection;
-import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.DataSource;
 import com.google.android.exoplayer2.upstream.DefaultAllocator;
-import com.google.android.exoplayer2.upstream.TransferListener;
-import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -72,7 +68,6 @@
     private final long mId;
 
     private final Handler.Callback mSourceReaderWorker;
-    private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
 
     private BufferManager.SampleBuffer mSampleBuffer;
     private Handler mSourceReaderHandler;
@@ -91,11 +86,10 @@
 
     public ExoPlayerSampleExtractor(
             Uri uri,
-            final DataSource source,
+            DataSource source,
             BufferManager bufferManager,
             PlaybackBufferListener bufferListener,
-            boolean isRecording,
-            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlagsoncurrentDvrPlaybackFlags) {
+            boolean isRecording) {
         this(
                 uri,
                 source,
@@ -103,8 +97,7 @@
                 bufferListener,
                 isRecording,
                 Looper.myLooper(),
-                new HandlerThread("SourceReaderThread"),
-                concurrentDvrPlaybackFlagsoncurrentDvrPlaybackFlags);
+                new HandlerThread("SourceReaderThread"));
     }
 
     @VisibleForTesting
@@ -116,88 +109,25 @@
             PlaybackBufferListener bufferListener,
             boolean isRecording,
             Looper workerLooper,
-            HandlerThread sourceReaderThread,
-            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) {
+            HandlerThread sourceReaderThread) {
         // It'll be used as a timeshift file chunk name's prefix.
         mId = System.currentTimeMillis();
-        mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
-
-        EventListener eventListener =
-                new EventListener() {
-                    @Override
-                    public void onLoadError(IOException error) {
-                        mError = error;
-                    }
-                };
 
         mSourceReaderThread = sourceReaderThread;
         mSourceReaderWorker =
                 new SourceReaderWorker(
                         new ExtractorMediaSource(
                                 uri,
-                                new com.google.android.exoplayer2.upstream.DataSource.Factory() {
-                                    @Override
-                                    public com.google.android.exoplayer2.upstream.DataSource
-                                            createDataSource() {
-                                        // Returns an adapter implementation for ExoPlayer V2
-                                        // DataSource interface.
-                                        return new com.google.android.exoplayer2.upstream
-                                                .DataSource() {
-
-                                            private @Nullable Uri uri;
-
-                                            // TODO: uncomment once this is part of the public API.
-                                            // @Override
-                                            public void addTransferListener(
-                                                    TransferListener transferListener) {
-                                                // Do nothing. Unsupported in V1.
-                                            }
-
-                                            @Override
-                                            public long open(DataSpec dataSpec) throws IOException {
-                                                this.uri = dataSpec.uri;
-                                                return source.open(
-                                                        new com.google.android.exoplayer.upstream
-                                                                .DataSpec(
-                                                                dataSpec.uri,
-                                                                dataSpec.postBody,
-                                                                dataSpec.absoluteStreamPosition,
-                                                                dataSpec.position,
-                                                                dataSpec.length,
-                                                                dataSpec.key,
-                                                                dataSpec.flags));
-                                            }
-
-                                            @Override
-                                            public int read(
-                                                    byte[] buffer, int offset, int readLength)
-                                                    throws IOException {
-                                                return source.read(buffer, offset, readLength);
-                                            }
-
-                                            @Override
-                                            public @Nullable Uri getUri() {
-                                                return uri;
-                                            }
-
-                                            @Override
-                                            public void close() throws IOException {
-                                                source.close();
-                                                uri = null;
-                                            }
-                                        };
-                                    }
-                                },
+                                /* dataSourceFactory= */ () -> source,
                                 new ExoPlayerExtractorsFactory(),
                                 new Handler(workerLooper),
-                                eventListener));
+                                /* eventListener= */ error -> mError = error));
         if (isRecording) {
             mSampleBuffer =
                     new RecordingSampleBuffer(
                             bufferManager,
                             bufferListener,
                             false,
-                            mConcurrentDvrPlaybackFlags,
                             RecordingSampleBuffer.BUFFER_REASON_RECORDING);
         } else {
             if (bufferManager == null) {
@@ -208,7 +138,6 @@
                                 bufferManager,
                                 bufferListener,
                                 true,
-                                mConcurrentDvrPlaybackFlags,
                                 RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK);
             }
         }
@@ -240,15 +169,11 @@
         public SourceReaderWorker(MediaSource sampleSource) {
             mSampleSource = sampleSource;
             mSampleSourceListener =
-                    new MediaSource.SourceInfoRefreshListener() {
-                        @Override
-                        public void onSourceInfoRefreshed(
-                                MediaSource source, Timeline timeline, Object manifest) {
-                            // Dynamic stream change is not supported yet. b/28169263
-                            // For now, this will cause EOS and playback reset.
-                        }
+                    (source, timeline, manifest) -> {
+                        // Dynamic stream change is not supported yet. b/28169263
+                        // For now, this will cause EOS and playback reset.
                     };
-            mSampleSource.prepareSource(null, false, mSampleSourceListener, null);
+            mSampleSource.prepareSource(mSampleSourceListener, null);
             mDecoderInputBuffer =
                     new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
             mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
@@ -365,9 +290,8 @@
                         mMediaPeriod =
                                 mSampleSource.createPeriod(
                                         new MediaSource.MediaPeriodId(0),
-                                        new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE)
-// AOSP_Comment_Out                                         , 0
-                                );
+                                        new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
+                                        0);
                         mMediaPeriod.prepare(this, 0);
                         try {
                             mMediaPeriod.maybeThrowPrepareError();
@@ -486,7 +410,7 @@
                         sample.data.position(0);
                         sample.data.put(mDecoderInputBuffer.data);
                         sample.data.flip();
-                        mPendingSamples.add(new Pair<>(index, sample));
+                        mPendingSamples.add(Pair.create(index, sample));
                         return;
                     }
                     mVideoTrackMet = true;
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java b/tuner/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java
index 9749e4b..d6685b2 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java
@@ -17,14 +17,16 @@
 package com.android.tv.tuner.exoplayer;
 
 import android.os.Handler;
+
 import com.android.tv.tuner.exoplayer.buffer.BufferManager;
 import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener;
 import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer;
+
 import com.google.android.exoplayer.MediaFormat;
 import com.google.android.exoplayer.MediaFormatHolder;
 import com.google.android.exoplayer.MediaFormatUtil;
 import com.google.android.exoplayer.SampleHolder;
-import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
@@ -44,15 +46,10 @@
     private final BufferManager mBufferManager;
     private final PlaybackBufferListener mBufferListener;
     private BufferManager.SampleBuffer mSampleBuffer;
-    private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
 
-    public FileSampleExtractor(
-            BufferManager bufferManager,
-            PlaybackBufferListener bufferListener,
-            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) {
+    public FileSampleExtractor(BufferManager bufferManager, PlaybackBufferListener bufferListener) {
         mBufferManager = bufferManager;
         mBufferListener = bufferListener;
-        mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
         mTrackCount = -1;
     }
 
@@ -80,7 +77,6 @@
                         mBufferManager,
                         mBufferListener,
                         true,
-                        mConcurrentDvrPlaybackFlags,
                         RecordingSampleBuffer.BUFFER_REASON_RECORDED_PLAYBACK);
         mSampleBuffer.init(ids, mTrackFormats);
         return true;
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
index 6781c61..67cf992 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
@@ -43,7 +43,8 @@
 import com.google.android.exoplayer.TrackRenderer;
 import com.google.android.exoplayer.audio.AudioCapabilities;
 import com.google.android.exoplayer.audio.AudioTrack;
-import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSource;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
@@ -147,7 +148,7 @@
      *
      * @param rendererBuilder the builder of track renderers
      * @param handler the handler for the playback events in track renderers
-     * @param sourceManager the manager for {@link DataSource}
+     * @param sourceManager the manager for {@link TsDataSource}
      * @param capabilities the {@link AudioCapabilities} of the current device
      * @param listener the listener for playback state changes
      */
@@ -214,7 +215,7 @@
     }
 
     /**
-     * Creates renderers and {@link DataSource} and initializes player.
+     * Creates renderers and {@link TsDataSource} and initializes player.
      *
      * @param context a {@link Context} instance
      * @param channel to play
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java
index e043907..a807adb 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java
@@ -17,6 +17,7 @@
 package com.android.tv.tuner.exoplayer;
 
 import android.content.Context;
+
 import com.android.tv.tuner.exoplayer.MpegTsPlayer.RendererBuilder;
 import com.android.tv.tuner.exoplayer.MpegTsPlayer.RendererBuilderCallback;
 import com.android.tv.tuner.exoplayer.audio.MpegTsDefaultAudioTrackRenderer;
@@ -25,25 +26,19 @@
 import com.google.android.exoplayer.MediaCodecSelector;
 import com.google.android.exoplayer.SampleSource;
 import com.google.android.exoplayer.TrackRenderer;
-import com.google.android.exoplayer.upstream.DataSource;
-import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+import com.google.android.exoplayer2.upstream.DataSource;
 
 /** Builder for renderer objects for {@link MpegTsPlayer}. */
 public class MpegTsRendererBuilder implements RendererBuilder {
     private final Context mContext;
     private final BufferManager mBufferManager;
     private final PlaybackBufferListener mBufferListener;
-    private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
 
     public MpegTsRendererBuilder(
-            Context context,
-            BufferManager bufferManager,
-            PlaybackBufferListener bufferListener,
-            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) {
+            Context context, BufferManager bufferManager, PlaybackBufferListener bufferListener) {
         mContext = context;
         mBufferManager = bufferManager;
         mBufferListener = bufferListener;
-        mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
     }
 
     @Override
@@ -52,13 +47,8 @@
         // Build the video and audio renderers.
         SampleExtractor extractor =
                 dataSource == null
-                        ? new MpegTsSampleExtractor(
-                                mBufferManager, mBufferListener, mConcurrentDvrPlaybackFlags)
-                        : new MpegTsSampleExtractor(
-                                dataSource,
-                                mBufferManager,
-                                mBufferListener,
-                                mConcurrentDvrPlaybackFlags);
+                        ? new MpegTsSampleExtractor(mBufferManager, mBufferListener)
+                        : new MpegTsSampleExtractor(dataSource, mBufferManager, mBufferListener);
         SampleSource sampleSource = new MpegTsSampleSource(extractor);
         MpegTsVideoTrackRenderer videoRenderer =
                 new MpegTsVideoTrackRenderer(
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java
index 582f18c..0b4f980 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java
@@ -18,6 +18,7 @@
 
 import android.net.Uri;
 import android.os.Handler;
+
 import com.android.tv.tuner.exoplayer.buffer.BufferManager;
 import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener;
 import com.android.tv.tuner.exoplayer.buffer.SamplePool;
@@ -25,9 +26,9 @@
 import com.google.android.exoplayer.MediaFormatHolder;
 import com.google.android.exoplayer.SampleHolder;
 import com.google.android.exoplayer.SampleSource;
-import com.google.android.exoplayer.upstream.DataSource;
 import com.google.android.exoplayer.util.MimeTypes;
-import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+import com.google.android.exoplayer2.upstream.DataSource;
+
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
@@ -59,27 +60,19 @@
     }
 
     /**
-     * Creates MpegTsSampleExtractor for {@link DataSource}.
+     * Creates MpegTsSampleExtractor for a {@link DataSource}.
      *
      * @param source the {@link DataSource} to extract from
      * @param bufferManager the manager for reading & writing samples backed by physical storage
      * @param bufferListener the {@link PlaybackBufferListener} to notify buffer storage status
-     * @param concurrentDvrPlaybackFlags
      */
     public MpegTsSampleExtractor(
             DataSource source,
             BufferManager bufferManager,
-            PlaybackBufferListener bufferListener,
-            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) {
-
+            PlaybackBufferListener bufferListener) {
         mSampleExtractor =
                 new ExoPlayerSampleExtractor(
-                        Uri.EMPTY,
-                        source,
-                        bufferManager,
-                        bufferListener,
-                        false,
-                        concurrentDvrPlaybackFlags);
+                        Uri.EMPTY, source, bufferManager, bufferListener, false);
         init();
     }
 
@@ -91,11 +84,8 @@
      *     change
      */
     public MpegTsSampleExtractor(
-            BufferManager bufferManager,
-            PlaybackBufferListener bufferListener,
-            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) {
-        mSampleExtractor =
-                new FileSampleExtractor(bufferManager, bufferListener, concurrentDvrPlaybackFlags);
+            BufferManager bufferManager, PlaybackBufferListener bufferListener) {
+        mSampleExtractor = new FileSampleExtractor(bufferManager, bufferListener);
         init();
     }
 
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java b/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java
index bab74c9..fb88e5b 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java
@@ -246,6 +246,7 @@
         mSource.seekToUs(positionUs);
         AUDIO_TRACK.reset();
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+            // b/21824483 workaround
             // resetSessionId() will create a new framework AudioTrack instead of reusing old one.
             AUDIO_TRACK.resetSessionId();
         }
@@ -284,6 +285,7 @@
                 // Ensure playback stops, after EoS was notified.
                 // Sometimes MediaCodecTrackRenderer does not fetch EoS timely
                 // after EoS was notified here long before.
+                // see b/21909113
                 long diff = SystemClock.elapsedRealtime() - mEndOfStreamMs;
                 if (diff >= KEEP_ALIVE_AFTER_EOS_DURATION_MS && !mIsStopped) {
                     throw new ExoPlaybackException("Much time has elapsed after EoS");
@@ -592,6 +594,7 @@
             }
             mCurrentPositionUs = Math.max(mPresentationTimeUs, mCurrentPositionUs);
         } else {
+            // TODO: Remove this workaround when b/22023809 is resolved.
             if (mPreviousPositionUs
                     > audioTrackCurrentPositionUs + BACKWARD_AUDIO_TRACK_MOVE_THRESHOLD_US) {
                 Log.e(
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
index c32540c..b8d8523 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
@@ -23,10 +23,13 @@
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Pair;
+
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.util.CommonUtils;
 import com.android.tv.tuner.exoplayer.SampleExtractor;
+
 import com.google.android.exoplayer.SampleHolder;
+
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -400,13 +403,13 @@
             SampleChunk sampleChunk =
                     mSampleChunkCreator.createSampleChunk(
                             samplePool, file, positionUs, mChunkCallback);
-            map.put(positionUs, new Pair(sampleChunk, 0));
+            map.put(positionUs, Pair.create(sampleChunk, 0));
             if (updateIndexFile) {
                 mStorageManager.updateIndexFile(id, map.size(), positionUs, sampleChunk, 0);
             }
             return sampleChunk;
         } else {
-            map.put(positionUs, new Pair(currentChunk, currentOffset));
+            map.put(positionUs, Pair.create(currentChunk, currentOffset));
             if (updateIndexFile) {
                 mStorageManager.updateIndexFile(
                         id, map.size(), positionUs, currentChunk, currentOffset);
@@ -447,7 +450,7 @@
                                 chunk);
                 basePositionUs = position.basePositionUs;
             }
-            map.put(position.positionUs, new Pair(chunk, position.offset));
+            map.put(position.positionUs, Pair.create(chunk, position.offset));
         }
     }
 
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
index f19756e..0e1cbe9 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
@@ -19,8 +19,7 @@
 import android.media.MediaFormat;
 import android.util.Log;
 import android.util.Pair;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
-import com.google.protobuf.nano.MessageNano;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
 import java.io.File;
@@ -369,7 +368,7 @@
                     META_FILE_TYPE_CAPTION + ((i == 0) ? META_FILE_SUFFIX : (i + META_FILE_SUFFIX));
             File file = new File(getBufferDir(), fileName);
             try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
-                out.write(MessageNano.toByteArray(track));
+                track.writeTo(out);
             } catch (Exception e) {
                 Log.e(TAG, "Fail to write caption info to files", e);
             }
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
index d95642c..00a3430 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
@@ -20,14 +20,16 @@
 import android.support.annotation.IntDef;
 import android.support.annotation.NonNull;
 import android.util.Log;
+
 import com.android.tv.tuner.exoplayer.MpegTsPlayer;
 import com.android.tv.tuner.exoplayer.SampleExtractor;
+
 import com.google.android.exoplayer.C;
 import com.google.android.exoplayer.MediaFormat;
 import com.google.android.exoplayer.SampleHolder;
 import com.google.android.exoplayer.SampleSource;
 import com.google.android.exoplayer.util.Assertions;
-import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+
 import java.io.IOException;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -69,7 +71,6 @@
     private final BufferManager mBufferManager;
     private final PlaybackBufferListener mBufferListener;
     private final @BufferReason int mBufferReason;
-    private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
 
     private int mTrackCount;
     private boolean[] mTrackSelected;
@@ -104,18 +105,15 @@
      * @param bufferManager the manager of {@link SampleChunk}
      * @param bufferListener the listener for buffer I/O event
      * @param enableTrickplay {@code true} when trickplay should be enabled
-     * @param concurrentDvrPlaybackFlags
      * @param bufferReason the reason for caching samples {@link BufferReason}
      */
     public RecordingSampleBuffer(
             BufferManager bufferManager,
             PlaybackBufferListener bufferListener,
             boolean enableTrickplay,
-            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
             @BufferReason int bufferReason) {
         mBufferManager = bufferManager;
         mBufferListener = bufferListener;
-        mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
         if (bufferListener != null) {
             bufferListener.onBufferStateChanged(enableTrickplay);
         }
@@ -133,13 +131,7 @@
         mReadSampleQueues = new ArrayList<>();
         mSampleChunkIoHelper =
                 new SampleChunkIoHelper(
-                        ids,
-                        mediaFormats,
-                        mBufferReason,
-                        mBufferManager,
-                        mSamplePool,
-                        mIoCallback,
-                        mConcurrentDvrPlaybackFlags);
+                        ids, mediaFormats, mBufferReason, mBufferManager, mSamplePool, mIoCallback);
         for (int i = 0; i < mTrackCount; ++i) {
             mReadSampleQueues.add(i, new SampleQueue(mSamplePool));
         }
diff --git a/tuner/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java b/tuner/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
index f4d3bf8..d16d080 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
@@ -24,12 +24,14 @@
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.Pair;
+
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer.BufferReason;
+
 import com.google.android.exoplayer.MediaFormat;
 import com.google.android.exoplayer.SampleHolder;
 import com.google.android.exoplayer.util.MimeTypes;
-import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.LinkedList;
@@ -64,7 +66,6 @@
     private final BufferManager mBufferManager;
     private final SamplePool mSamplePool;
     private final IoCallback mIoCallback;
-    private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
 
     private Handler mIoHandler;
     private final ConcurrentLinkedQueue<SampleHolder> mReadSampleBuffers[];
@@ -121,7 +122,6 @@
      * @param bufferManager manager of {@link SampleChunk} collections
      * @param samplePool allocator for a sample
      * @param ioCallback listeners for I/O events
-     * @param concurrentDvrPlaybackFlags
      */
     public SampleChunkIoHelper(
             List<String> ids,
@@ -129,8 +129,7 @@
             @BufferReason int bufferReason,
             BufferManager bufferManager,
             SamplePool samplePool,
-            IoCallback ioCallback,
-            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags) {
+            IoCallback ioCallback) {
         mTrackCount = ids.size();
         mIds = ids;
         mMediaFormats = mediaFormats;
@@ -138,7 +137,6 @@
         mBufferManager = bufferManager;
         mSamplePool = samplePool;
         mIoCallback = ioCallback;
-        mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
 
         mReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount];
         mHandlerReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount];
@@ -184,9 +182,7 @@
         }
 
         try {
-            if (mConcurrentDvrPlaybackFlags.enabled()
-                    && mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDING
-                    && mTrackCount > 0) {
+            if (mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDING && mTrackCount > 0) {
                 // Saves meta information for recording.
                 List<BufferManager.TrackFormat> audios = new ArrayList<>(mTrackCount);
                 List<BufferManager.TrackFormat> videos = new ArrayList<>(mTrackCount);
@@ -384,8 +380,7 @@
 
     private void doOpenWrite(int index) throws IOException {
         boolean updateIndexFile =
-                mConcurrentDvrPlaybackFlags.enabled()
-                        && (mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDING)
+                (mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDING)
                         && (MimeTypes.isVideo(mMediaFormats.get(index).mimeType)
                                 || MimeTypes.isAudio(mMediaFormats.get(index).mimeType));
 
@@ -426,13 +421,10 @@
             SampleHolder sample = mReadIoStates[index].read();
             if (sample != null) {
                 mHandlerReadSampleBuffers[index].offer(sample);
-                if (mConcurrentDvrPlaybackFlags.enabled()) {
-                    mReadChunkOffset[index] = mReadIoStates[index].getOffset();
-                    mReadChunkPositionUs[index] = sample.timeUs;
-                }
+                mReadChunkOffset[index] = mReadIoStates[index].getOffset();
+                mReadChunkPositionUs[index] = sample.timeUs;
             } else {
-                if (mConcurrentDvrPlaybackFlags.enabled()
-                        && mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDED_PLAYBACK) {
+                if (mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDED_PLAYBACK) {
                     // Update Index, to load new Samples
                     updateIndex(index, mReadChunkOffset[index]);
                 }
@@ -485,9 +477,7 @@
                                     : mWriteIoStates[params.index].getChunk();
                     int currentOffset = (int) mWriteIoStates[params.index].getOffset();
                     boolean updateIndexFile =
-                            mConcurrentDvrPlaybackFlags.enabled()
-                                    && (mBufferReason
-                                            == RecordingSampleBuffer.BUFFER_REASON_RECORDING)
+                            (mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDING)
                                     && (MimeTypes.isVideo(mMediaFormats.get(index).mimeType)
                                             || MimeTypes.isAudio(
                                                     mMediaFormats.get(index).mimeType));
diff --git a/tuner/src/com/android/tv/tuner/exoplayer2/VideoRendererExoV2.java b/tuner/src/com/android/tv/tuner/exoplayer2/VideoRendererExoV2.java
index 1203900..a71352f 100644
--- a/tuner/src/com/android/tv/tuner/exoplayer2/VideoRendererExoV2.java
+++ b/tuner/src/com/android/tv/tuner/exoplayer2/VideoRendererExoV2.java
@@ -24,7 +24,6 @@
 import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
 import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
 import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
-import com.google.android.exoplayer2.util.MimeTypes;
 import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
 import com.google.android.exoplayer2.video.VideoRendererEventListener;
 import java.lang.reflect.Field;
@@ -48,8 +47,8 @@
     private static final String SOFTWARE_DECODER_NAME_PREFIX = "OMX.google.";
     private static final long ALLOWED_JOINING_TIME_MS = 5000;
     private static final int DROPPED_FRAMES_NOTIFICATION_THRESHOLD = 10;
-    private static final int MIN_HD_HEIGHT = 720;
-    private static Field sRenderedFirstFrameField;
+  // private static final int MIN_HD_HEIGHT = 720;
+  private static Field sRenderedFirstFrameField;
 
     private final boolean mIsSwCodecEnabled;
     private boolean mCodecIsSwPreferred;
@@ -108,16 +107,18 @@
         return decoderInfos;
     }
 
-    @Override
-    protected void onInputFormatChanged(Format format) throws ExoPlaybackException {
-        mCodecIsSwPreferred =
-                MimeTypes.VIDEO_MPEG2.equals(format.sampleMimeType)
-                        && format.height < MIN_HD_HEIGHT;
-        super.onInputFormatChanged(format);
-    }
+  // TODO: Uncomment once ExoPlayer v2.10.0 is released [Internal ref: b/130625979].
+  // @Override
+  // protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException {
+  //     Format format = formatHolder.format;
+  //     mCodecIsSwPreferred =
+  //             MimeTypes.VIDEO_MPEG2.equals(format.sampleMimeType)
+  //                     && format.height < MIN_HD_HEIGHT;
+  //     super.onInputFormatChanged(format);
+  // }
 
-    @Override
-    protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
+  @Override
+  protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
         super.onPositionReset(positionUs, joining);
         // Disabling pre-rendering of the first frame in order to avoid a frozen picture when
         // starting the playback. We do this only once, when the renderer is enabled at first, since
diff --git a/tuner/src/com/android/tv/tuner/features/TunerFeatures.java b/tuner/src/com/android/tv/tuner/features/TunerFeatures.java
index 6033a3a..6ee5aa8 100644
--- a/tuner/src/com/android/tv/tuner/features/TunerFeatures.java
+++ b/tuner/src/com/android/tv/tuner/features/TunerFeatures.java
@@ -19,9 +19,9 @@
 import static com.android.tv.common.feature.FeatureUtils.OFF;
 
 import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.feature.DeveloperPreferenceFeature;
 import com.android.tv.common.feature.Feature;
 import com.android.tv.common.feature.Model;
-import com.android.tv.common.feature.PropertyFeature;
 import com.android.tv.common.feature.Sdk;
 
 /**
@@ -39,10 +39,11 @@
      * <p>Prefer software based codec for SD channels.
      */
     public static final Feature USE_SW_CODEC_FOR_SD =
-            PropertyFeature.create(
+            DeveloperPreferenceFeature.create(
                     "use_sw_codec_for_sd",
-                    false
-                    );
+                    // On Nexus Player, SW codec is better than HW codec in terms of picture
+                    // quality.
+                    Model.NEXUS_PLAYER.isEnabled());
 
     /**
      * Does the TvProvider on the installed device allow systems inserts to the programs table.
diff --git a/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunChannelScan.java b/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunChannelScan.java
new file mode 100644
index 0000000..38610dd
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunChannelScan.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.hdhomerun;
+
+import android.content.Context;
+import android.media.tv.TvContract;
+import android.os.ConditionVariable;
+import android.util.Log;
+import android.util.Xml;
+import com.android.tv.tuner.api.ChannelScanListener;
+import com.android.tv.tuner.data.TunerChannel;
+import com.android.tv.tuner.ts.EventDetector.EventListener;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.regex.Pattern;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+/** A helper class to perform channel scan on HDHomeRun tuner. */
+public class HdHomeRunChannelScan {
+    private static final String TAG = "HdHomeRunChannelScan";
+    private static final boolean DEBUG = false;
+
+    private static final String LINEUP_FILENAME = "lineup.xml";
+    private static final String NAME_LINEUP = "Lineup";
+    private static final String NAME_PROGRAM = "Program";
+    private static final String NAME_GUIDE_NUMBER = "GuideNumber";
+    private static final String NAME_GUIDE_NAME = "GuideName";
+    private static final String NAME_HD = "HD";
+    private static final String NAME_TAGS = "Tags";
+    private static final String NAME_DRM = "DRM";
+
+    private final Context mContext;
+    private final ChannelScanListener mEventListener;
+    private final HdHomeRunTunerHal mTunerHal;
+    private int mProgramCount;
+
+    public HdHomeRunChannelScan(
+            Context context, EventListener eventListener, HdHomeRunTunerHal hal) {
+        mContext = context;
+        mEventListener = eventListener;
+        mTunerHal = hal;
+    }
+
+    public void scan(ConditionVariable conditionStopped) {
+        String urlString = "http://" + mTunerHal.getIpAddress() + "/" + LINEUP_FILENAME;
+        if (DEBUG) Log.d(TAG, "Reading " + urlString);
+        URL url;
+        HttpURLConnection connection = null;
+        InputStream inputStream;
+        try {
+            url = new URL(urlString);
+            connection = (HttpURLConnection) url.openConnection();
+            connection.setReadTimeout(HdHomeRunTunerHal.READ_TIMEOUT_MS_FOR_URLCONNECTION);
+            connection.setConnectTimeout(HdHomeRunTunerHal.CONNECTION_TIMEOUT_MS_FOR_URLCONNECTION);
+            connection.setRequestMethod("GET");
+            connection.setDoInput(true);
+            connection.connect();
+            inputStream = connection.getInputStream();
+        } catch (IOException e) {
+            Log.e(TAG, "Connection failed: " + urlString, e);
+            if (connection != null) {
+                connection.disconnect();
+            }
+            return;
+        }
+        if (conditionStopped.block(-1)) {
+            try {
+                inputStream.close();
+            } catch (IOException e) {
+                // Does nothing.
+            }
+            connection.disconnect();
+            return;
+        }
+
+        XmlPullParser parser = Xml.newPullParser();
+        try {
+            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
+            parser.setInput(inputStream, null);
+            parser.nextTag();
+            parser.require(XmlPullParser.START_TAG, null, NAME_LINEUP);
+            while (parser.next() != XmlPullParser.END_TAG) {
+                if (conditionStopped.block(-1)) {
+                    break;
+                }
+                if (parser.getEventType() != XmlPullParser.START_TAG) {
+                    continue;
+                }
+                String name = parser.getName();
+                // Starts by looking for the program tag
+                if (name.equals(NAME_PROGRAM)) {
+                    readProgram(parser);
+                } else {
+                    skip(parser);
+                }
+            }
+            inputStream.close();
+        } catch (IOException | XmlPullParserException e) {
+            Log.e(TAG, "Parse error", e);
+        }
+        connection.disconnect();
+        mTunerHal.markAsScannedDevice(mContext);
+    }
+
+    private void readProgram(XmlPullParser parser) throws XmlPullParserException, IOException {
+        parser.require(XmlPullParser.START_TAG, null, NAME_PROGRAM);
+        String guideNumber = "";
+        String guideName = "";
+        String videoFormat = null;
+        String tags = "";
+        boolean recordingProhibited = false;
+        while (parser.next() != XmlPullParser.END_TAG) {
+            if (parser.getEventType() != XmlPullParser.START_TAG) {
+                continue;
+            }
+            String name = parser.getName();
+            if (name.equals(NAME_GUIDE_NUMBER)) {
+                guideNumber = readText(parser, NAME_GUIDE_NUMBER);
+            } else if (name.equals(NAME_GUIDE_NAME)) {
+                guideName = readText(parser, NAME_GUIDE_NAME);
+            } else if (name.equals(NAME_HD)) {
+                videoFormat = TvContract.Channels.VIDEO_FORMAT_720P;
+                skip(parser);
+            } else if (name.equals(NAME_TAGS)) {
+                tags = readText(parser, NAME_TAGS);
+            } else if (name.equals(NAME_DRM)) {
+                String drm = readText(parser, NAME_DRM);
+                try {
+                    recordingProhibited = (Integer.parseInt(drm)) != 0;
+                } catch (NumberFormatException e) {
+                    Log.e(TAG, "Load DRM property failed: illegal number: " + drm);
+                    // If DRM property is present, we treat it as copy-once or copy-never.
+                    recordingProhibited = true;
+                }
+            } else {
+                skip(parser);
+            }
+        }
+        if (!tags.isEmpty()) {
+            // Skip encrypted channels since we don't know how to decrypt them.
+            return;
+        }
+        int major;
+        int minor = 0;
+        final String separator = Character.toString(HdHomeRunTunerHal.VCHANNEL_SEPARATOR);
+        if (guideNumber.contains(separator)) {
+            String[] parts = guideNumber.split(Pattern.quote(separator));
+            major = Integer.parseInt(parts[0]);
+            minor = Integer.parseInt(parts[1]);
+        } else {
+            major = Integer.parseInt(guideNumber);
+        }
+        // Need to assign a unique program number (i.e. mProgramCount) to avoid being duplicated.
+        mEventListener.onChannelDetected(
+                TunerChannel.forNetwork(
+                        major, minor, mProgramCount++, guideName, recordingProhibited, videoFormat),
+                true);
+    }
+
+    private String readText(XmlPullParser parser, String name)
+            throws IOException, XmlPullParserException {
+        String result = "";
+        parser.require(XmlPullParser.START_TAG, null, name);
+        if (parser.next() == XmlPullParser.TEXT) {
+            result = parser.getText();
+            parser.nextTag();
+        }
+        parser.require(XmlPullParser.END_TAG, null, name);
+        if (DEBUG) Log.d(TAG, "<" + name + ">=" + result);
+        return result;
+    }
+
+    private void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
+        if (parser.getEventType() != XmlPullParser.START_TAG) {
+            throw new IllegalStateException();
+        }
+        int depth = 1;
+        while (depth != 0) {
+            switch (parser.next()) {
+                case XmlPullParser.END_TAG:
+                    depth--;
+                    break;
+                case XmlPullParser.START_TAG:
+                    depth++;
+                    break;
+            }
+        }
+    }
+}
diff --git a/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunControlSocket.java b/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunControlSocket.java
new file mode 100644
index 0000000..ce7c518
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunControlSocket.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.hdhomerun;
+
+import android.support.annotation.Nullable;
+import android.util.Log;
+import android.util.Pair;
+import com.android.tv.tuner.hdhomerun.HdHomeRunDiscover.HdHomeRunDiscoverDevice;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A class to send/receive control commands and results to/from HDHomeRun devices via TCP sockets.
+ * {@link #close()} method should be called after usage to close the TCP socket.
+ */
+class HdHomeRunControlSocket implements AutoCloseable {
+    private static final String TAG = "HdHomeRunControlSocket";
+    private static final boolean DEBUG = false;
+
+    private int mDesiredDeviceId;
+    private int mDesiredDeviceIp;
+    private int mActualDeviceId;
+    private int mActualDeviceIp;
+    private Socket mSocket;
+
+    HdHomeRunControlSocket(int deviceId, int deviceIp) {
+        mDesiredDeviceId = deviceId;
+        mDesiredDeviceIp = deviceIp;
+        mActualDeviceId = 0;
+        mActualDeviceIp = 0;
+    }
+
+    /**
+     * Gets control settings from HDHomeRun devices.
+     *
+     * @param name the name of the field whose value we want to get.
+     */
+    @Nullable
+    String get(String name) {
+        byte[] data = new byte[name.length() + 3];
+        ByteBuffer buffer = ByteBuffer.wrap(data);
+        buffer.put(HdHomeRunUtils.HDHOMERUN_TAG_GETSET_NAME);
+        buffer.put((byte) (name.length() + 1));
+        buffer.put(name.getBytes());
+
+        // Send & Receive.
+        byte[] result =
+                sendAndReceive(
+                        data,
+                        HdHomeRunUtils.HDHOMERUN_TYPE_GETSET_REQUEST,
+                        HdHomeRunUtils.HDHOMERUN_CONTROL_RECEIVE_TIMEOUT_MS);
+        if (result == null) {
+            if (DEBUG) Log.d(TAG, "Cannot get result for " + name);
+            return null;
+        }
+
+        // Response.
+        buffer = ByteBuffer.wrap(result);
+        while (true) {
+            Pair<Byte, byte[]> tagAndValue = HdHomeRunUtils.readTaggedValue(buffer);
+            if (tagAndValue == null) {
+                break;
+            }
+            switch (tagAndValue.first) {
+                case HdHomeRunUtils.HDHOMERUN_TAG_GETSET_VALUE:
+                    // Removes the 0 tail.
+                    return new String(
+                            Arrays.copyOfRange(
+                                    tagAndValue.second, 0, tagAndValue.second.length - 1));
+                case HdHomeRunUtils.HDHOMERUN_TAG_ERROR_MESSAGE:
+                    return null;
+            }
+        }
+        return null;
+    }
+
+    /** Gets ID of HDHomeRun devices. */
+    int getDeviceId() {
+        if (!connectAndUpdateDeviceInfo()) {
+            return 0;
+        }
+        return mActualDeviceId;
+    }
+
+    private boolean connectAndUpdateDeviceInfo() {
+        if (mSocket != null) {
+            return true;
+        }
+        if ((mDesiredDeviceId == 0) && (mDesiredDeviceIp == 0)) {
+            if (DEBUG) Log.d(TAG, "Desired ID and IP cannot be both zero.");
+            return false;
+        }
+        if (HdHomeRunUtils.isIpMulticast(mDesiredDeviceIp)) {
+            if (DEBUG) Log.d(TAG, "IP cannot be multicast IP.");
+            return false;
+        }
+
+        // Find device.
+        List<HdHomeRunDiscoverDevice> result =
+                HdHomeRunUtils.findHdHomeRunDevices(
+                        mDesiredDeviceIp,
+                        HdHomeRunUtils.HDHOMERUN_DEVICE_TYPE_WILDCARD,
+                        mDesiredDeviceId,
+                        1);
+        if (result.isEmpty()) {
+            if (DEBUG) Log.d(TAG, "Cannot find device on: " + mDesiredDeviceIp);
+            return false;
+        }
+        mActualDeviceIp = result.get(0).mIpAddress;
+        mActualDeviceId = result.get(0).mDeviceId;
+
+        // Create socket and initiate connection.
+        mSocket = new Socket();
+        try {
+            mSocket.connect(
+                    new InetSocketAddress(
+                            HdHomeRunUtils.intToAddress(mActualDeviceIp),
+                            HdHomeRunUtils.HDHOMERUN_CONTROL_TCP_PORT),
+                    HdHomeRunUtils.HDHOMERUN_CONTROL_CONNECT_TIMEOUT_MS);
+        } catch (IOException e) {
+            if (DEBUG) Log.d(TAG, "Cannot connect to socket: " + mSocket);
+            mSocket = null;
+            return false;
+        }
+
+        // Success.
+        Log.i(TAG, "Connected to socket: " + mSocket);
+        return true;
+    }
+
+    private byte[] sendAndReceive(byte[] data, short type, int timeout) {
+        byte[] sealedData = HdHomeRunUtils.sealFrame(data, type);
+        for (int i = 0; i < 2; i++) {
+            if (mSocket == null && !connectAndUpdateDeviceInfo()) {
+                return null;
+            }
+            if (!send(sealedData)) {
+                continue;
+            }
+            Pair<Short, byte[]> receivedData = receive(timeout);
+            if (receivedData == null || receivedData.first == null) {
+                continue;
+            }
+            if (receivedData.first != type + 1) {
+                if (DEBUG) Log.d(TAG, "Returned type incorrect: " + receivedData.first);
+                close();
+                continue;
+            }
+            return receivedData.second;
+        }
+        return null;
+    }
+
+    private boolean send(byte[] data) {
+        try {
+            OutputStream out = mSocket.getOutputStream();
+            mSocket.setSoTimeout(HdHomeRunUtils.HDHOMERUN_CONTROL_SEND_TIMEOUT_MS);
+            out.write(data);
+        } catch (IOException e) {
+            if (DEBUG) Log.d(TAG, "Cannot send packet to socket: " + mSocket);
+            close();
+            return false;
+        }
+        return true;
+    }
+
+    private Pair<Short, byte[]> receive(int timeout) {
+        byte[] receivedData = new byte[3074];
+        try {
+            InputStream input = mSocket.getInputStream();
+            mSocket.setSoTimeout(timeout);
+            int index = 0;
+            long startTime = System.currentTimeMillis();
+            while (System.currentTimeMillis() - startTime < timeout) {
+                int length = receivedData.length - index;
+                index += input.read(receivedData, index, length);
+                Pair<Short, byte[]> result = HdHomeRunUtils.openFrame(receivedData, index);
+                if (result != null) {
+                    if (result.first == HdHomeRunUtils.HDHOMERUN_TYPE_INVALID) {
+                        if (DEBUG) Log.d(TAG, "Returned type is invalid.");
+                        close();
+                        return null;
+                    }
+                    return result;
+                }
+                if (DEBUG) Log.d(TAG, "Received result is null!");
+            }
+        } catch (IOException e) {
+            if (DEBUG) Log.d(TAG, "Cannot receive from socket: " + mSocket);
+            close();
+        }
+        return null;
+    }
+
+    @Override
+    public void close() {
+        if (mSocket != null) {
+            try {
+                mSocket.close();
+            } catch (IOException e) {
+                // Do nothing
+            }
+            mSocket = null;
+        }
+    }
+}
diff --git a/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunDevice.java b/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunDevice.java
new file mode 100644
index 0000000..dcf87ca
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunDevice.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.hdhomerun;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+/**
+ * An HDHomeRun device detected on the network. This abstraction only contains network data
+ * necessary to establish a connection with the device and does not represent a communication
+ * channel with the device itself. Currently, we only support devices with HTTP streaming
+ * functionality.
+ */
+public class HdHomeRunDevice implements Parcelable {
+    private int mIpAddress;
+    private int mDeviceType;
+    private int mDeviceId;
+    private int mTunerIndex;
+    private String mDeviceModel;
+
+    /**
+     * Creates {@code HdHomeRunDevice} object from a parcel.
+     *
+     * @param parcel The parcel to create {@code HdHomeRunDevice} object from.
+     */
+    public HdHomeRunDevice(Parcel parcel) {
+        mIpAddress = parcel.readInt();
+        mDeviceType = parcel.readInt();
+        mDeviceId = parcel.readInt();
+        mTunerIndex = parcel.readInt();
+        mDeviceModel = parcel.readString();
+    }
+
+    /**
+     * Creates {@code HdHomeRunDevice} object from IP address, device type, device ID and tuner
+     * index.
+     *
+     * @param ipAddress The IP address to create {@code HdHomeRunDevice} object from.
+     * @param deviceType The device type to create {@code HdHomeRunDevice} object from.
+     * @param deviceId The device ID to create {@code HdHomeRunDevice} object from.
+     * @param tunerIndex The tuner index to {@code HdHomeRunDevice} object from.
+     */
+    public HdHomeRunDevice(
+            int ipAddress, int deviceType, int deviceId, int tunerIndex, String deviceModel) {
+        mIpAddress = ipAddress;
+        mDeviceType = deviceType;
+        mDeviceId = deviceId;
+        mTunerIndex = tunerIndex;
+        mDeviceModel = deviceModel;
+    }
+
+    /**
+     * Returns the IP address.
+     *
+     * @return the IP address of this homerun device.
+     */
+    public int getIpAddress() {
+        return mIpAddress;
+    }
+
+    /**
+     * Returns the device type.
+     *
+     * @return the type of device for this homerun device.
+     */
+    public int getDeviceType() {
+        return mDeviceType;
+    }
+
+    /**
+     * Returns the device ID.
+     *
+     * @return the device ID of this homerun device.
+     */
+    public int getDeviceId() {
+        return mDeviceId;
+    }
+
+    /**
+     * Returns the tuner index.
+     *
+     * @return the tuner index of this homerun device.
+     */
+    public int getTunerIndex() {
+        return mTunerIndex;
+    }
+
+    /**
+     * Returns the device model.
+     *
+     * @return the device model of this homerun device.
+     */
+    public String getDeviceModel() {
+        return mDeviceModel;
+    }
+
+    @Override
+    public String toString() {
+        String ipAddress =
+                ""
+                        + ((mIpAddress >>> 24) & 0xff)
+                        + "."
+                        + ((mIpAddress >>> 16) & 0xff)
+                        + "."
+                        + ((mIpAddress >>> 8) & 0xff)
+                        + "."
+                        + (mIpAddress & 0xff);
+        return String.format("[%x-%d:%s]", mDeviceId, mTunerIndex, ipAddress);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(mIpAddress);
+        out.writeInt(mDeviceType);
+        out.writeInt(mDeviceId);
+        out.writeInt(mTunerIndex);
+        out.writeString(mDeviceModel);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 17;
+        hash = hash * 31 + getIpAddress();
+        hash = hash * 31 + getDeviceType();
+        hash = hash * 31 + getDeviceId();
+        hash = hash * 31 + getTunerIndex();
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof HdHomeRunDevice)) {
+            return false;
+        }
+        HdHomeRunDevice rhs = (HdHomeRunDevice) o;
+        return rhs != null
+                && getIpAddress() == rhs.getIpAddress()
+                && getDeviceType() == rhs.getDeviceType()
+                && getDeviceId() == rhs.getDeviceId()
+                && getTunerIndex() == rhs.getTunerIndex()
+                && TextUtils.equals(getDeviceModel(), rhs.getDeviceModel());
+    }
+
+    public static final Parcelable.Creator<HdHomeRunDevice> CREATOR =
+            new Parcelable.Creator<HdHomeRunDevice>() {
+
+                @Override
+                public HdHomeRunDevice createFromParcel(Parcel in) {
+                    return new HdHomeRunDevice(in);
+                }
+
+                @Override
+                public HdHomeRunDevice[] newArray(int size) {
+                    return new HdHomeRunDevice[size];
+                }
+            };
+}
diff --git a/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunDiscover.java b/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunDiscover.java
new file mode 100644
index 0000000..85b3450
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunDiscover.java
@@ -0,0 +1,446 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.hdhomerun;
+
+import android.support.annotation.NonNull;
+import android.util.Log;
+import android.util.Pair;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+
+/** A class to discover HDHomeRun devices on the network with UDP broadcasting. */
+class HdHomeRunDiscover {
+    private static final String TAG = "HdHomeRunDiscover";
+    private static final boolean DEBUG = false;
+
+    private static final int HDHOMERUN_DISCOVER_MAX_SOCK_COUNT = 16;
+    private static final int HDHOMERUN_DISCOVER_RETRY_LIMIT = 2;
+    private static final int HDHOMERUN_DISCOVER_TIMEOUT_MS = 500;
+    private static final int HDHOMERUN_DISCOVER_RECEIVE_WAITE_TIME_MS = 10;
+
+    private List<HdHomeRunDiscoverSocket> mSockets = new ArrayList<>();
+
+    /** Creates a discover object. If cannot add a default socket, return {@code null}. */
+    static HdHomeRunDiscover create() {
+        HdHomeRunDiscover hdHomeRunDiscover = new HdHomeRunDiscover();
+        // Create a routable socket (always first entry).
+        if (!hdHomeRunDiscover.addSocket(0, 0)) {
+            return null;
+        }
+        return hdHomeRunDiscover;
+    }
+
+    /** Closes and releases all sockets required by this discover object. */
+    void close() {
+        for (HdHomeRunDiscoverSocket discoverSocket : mSockets) {
+            discoverSocket.close();
+        }
+    }
+
+    /** Finds HDHomeRun devices. */
+    @NonNull
+    List<HdHomeRunDiscoverDevice> findDevices(
+            int targetIp, int deviceType, int deviceId, int maxCount) {
+        List<HdHomeRunDiscoverDevice> resultList = new ArrayList<>();
+        resetLocalIpSockets();
+        for (int retry = 0;
+                retry < HDHOMERUN_DISCOVER_RETRY_LIMIT && resultList.isEmpty();
+                retry++) {
+            int localIpSent = send(targetIp, deviceType, deviceId);
+            if (localIpSent == 0) {
+                if (DEBUG) {
+                    Log.d(TAG, "Cannot send to target ip: " + HdHomeRunUtils.getIpString(targetIp));
+                }
+                continue;
+            }
+            long timeout = System.currentTimeMillis() + HDHOMERUN_DISCOVER_TIMEOUT_MS * localIpSent;
+            while (System.currentTimeMillis() < timeout) {
+                HdHomeRunDiscoverDevice result = new HdHomeRunDiscoverDevice();
+                if (!receive(result)) {
+                    continue;
+                }
+                // Filter.
+                if (deviceType != HdHomeRunUtils.HDHOMERUN_DEVICE_TYPE_WILDCARD
+                        && deviceType != result.mDeviceType) {
+                    continue;
+                }
+                if (deviceId != HdHomeRunUtils.HDHOMERUN_DEVICE_ID_WILDCARD
+                        && deviceId != result.mDeviceId) {
+                    continue;
+                }
+                if (isObsoleteDevice(deviceId)) {
+                    continue;
+                }
+                // Ensure not already in list.
+                if (resultList.contains(result)) {
+                    continue;
+                }
+                // Add to list.
+                resultList.add(result);
+                if (resultList.size() >= maxCount) {
+                    break;
+                }
+            }
+        }
+        return resultList;
+    }
+
+    private boolean addSocket(int localIp, int subnetMask) {
+        for (int i = 1; i < mSockets.size(); i++) {
+            HdHomeRunDiscoverSocket discoverSocket = mSockets.get(i);
+            if ((discoverSocket.mLocalIp == localIp)
+                    && (discoverSocket.mSubnetMask == subnetMask)) {
+                discoverSocket.mDetected = true;
+                return true;
+            }
+        }
+        if (mSockets.size() >= HDHOMERUN_DISCOVER_MAX_SOCK_COUNT) {
+            return false;
+        }
+        DatagramSocket socket;
+        try {
+            socket = new DatagramSocket(0, HdHomeRunUtils.intToAddress(localIp));
+            socket.setBroadcast(true);
+        } catch (IOException e) {
+            if (DEBUG) Log.d(TAG, "Cannot create socket: " + HdHomeRunUtils.getIpString(localIp));
+            return false;
+        }
+        // Write socket entry.
+        mSockets.add(new HdHomeRunDiscoverSocket(socket, true, localIp, subnetMask));
+        return true;
+    }
+
+    private void resetLocalIpSockets() {
+        for (int i = 1; i < mSockets.size(); i++) {
+            mSockets.get(i).mDetected = false;
+            mSockets.get(i).mDiscoverPacketSent = false;
+        }
+        List<LocalIpInfo> ipInfoList = getLocalIpInfo(HDHOMERUN_DISCOVER_MAX_SOCK_COUNT);
+        for (LocalIpInfo ipInfo : ipInfoList) {
+            if (DEBUG) {
+                Log.d(
+                        TAG,
+                        "Add local IP: "
+                                + HdHomeRunUtils.getIpString(ipInfo.mIpAddress)
+                                + ", "
+                                + HdHomeRunUtils.getIpString(ipInfo.mSubnetMask));
+            }
+            addSocket(ipInfo.mIpAddress, ipInfo.mSubnetMask);
+        }
+        Iterator<HdHomeRunDiscoverSocket> iterator = mSockets.iterator();
+        while (iterator.hasNext()) {
+            HdHomeRunDiscoverSocket discoverSocket = iterator.next();
+            if (!discoverSocket.mDetected) {
+                discoverSocket.close();
+                iterator.remove();
+            }
+        }
+    }
+
+    private List<LocalIpInfo> getLocalIpInfo(int maxCount) {
+        Enumeration<NetworkInterface> interfaces;
+        try {
+            interfaces = NetworkInterface.getNetworkInterfaces();
+        } catch (SocketException e) {
+            return Collections.emptyList();
+        }
+        List<LocalIpInfo> result = new ArrayList<>();
+        while (interfaces.hasMoreElements()) {
+            NetworkInterface networkInterface = interfaces.nextElement();
+            for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
+                InetAddress inetAddress = interfaceAddress.getAddress();
+                if (!inetAddress.isAnyLocalAddress()
+                        && !inetAddress.isLinkLocalAddress()
+                        && !inetAddress.isLoopbackAddress()
+                        && !inetAddress.isMulticastAddress()) {
+                    LocalIpInfo localIpInfo = new LocalIpInfo();
+                    localIpInfo.mIpAddress = HdHomeRunUtils.addressToInt(inetAddress.getAddress());
+                    localIpInfo.mSubnetMask =
+                            (0x7fffffff >> (31 - interfaceAddress.getNetworkPrefixLength()));
+                    result.add(localIpInfo);
+                    if (result.size() >= maxCount) {
+                        return result;
+                    }
+                }
+            }
+        }
+        return result;
+    }
+
+    private int send(int targetIp, int deviceType, int deviceId) {
+        return targetIp == 0
+                ? sendWildcardIp(deviceType, deviceId)
+                : sendTargetIp(targetIp, deviceType, deviceId);
+    }
+
+    private int sendWildcardIp(int deviceType, int deviceId) {
+        int localIpSent = 0;
+
+        // Send subnet broadcast using each local ip socket.
+        // This will work with multiple separate 169.254.x.x interfaces.
+        for (int i = 1; i < mSockets.size(); i++) {
+            HdHomeRunDiscoverSocket discoverSocket = mSockets.get(i);
+            int targetIp = discoverSocket.mLocalIp | ~discoverSocket.mSubnetMask;
+            if (DEBUG) Log.d(TAG, "Send: " + HdHomeRunUtils.getIpString(targetIp));
+            localIpSent += discoverSocket.send(targetIp, deviceType, deviceId) ? 1 : 0;
+        }
+        // If no local ip sockets then fall back to sending a global broadcast letting
+        // the OS choose the interface.
+        if (localIpSent == 0) {
+            if (DEBUG) Log.d(TAG, "Send: " + HdHomeRunUtils.getIpString(0xFFFFFFFF));
+            localIpSent = mSockets.get(0).send(0xFFFFFFFF, deviceType, deviceId) ? 1 : 0;
+        }
+        return localIpSent;
+    }
+
+    private int sendTargetIp(int targetIp, int deviceType, int deviceId) {
+        int localIpSent = 0;
+
+        // Send targeted packet from any local ip that is in the same subnet.
+        // This will work with multiple separate 169.254.x.x interfaces.
+        for (int i = 1; i < mSockets.size(); i++) {
+            HdHomeRunDiscoverSocket discoverSocket = mSockets.get(i);
+            if (discoverSocket.mSubnetMask == 0) {
+                continue;
+            }
+            if ((targetIp & discoverSocket.mSubnetMask)
+                    != (discoverSocket.mLocalIp & discoverSocket.mSubnetMask)) {
+                continue;
+            }
+            localIpSent += discoverSocket.send(targetIp, deviceType, deviceId) ? 1 : 0;
+        }
+        // If target IP does not match a local subnet then fall back to letting the OS choose
+        // the gateway interface.
+        if (localIpSent == 0) {
+            localIpSent = mSockets.get(0).send(targetIp, deviceType, deviceId) ? 1 : 0;
+        }
+        return localIpSent;
+    }
+
+    private boolean receive(HdHomeRunDiscoverDevice result) {
+        for (HdHomeRunDiscoverSocket discoverSocket : mSockets) {
+            if (discoverSocket.mDiscoverPacketSent && discoverSocket.receive(result)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean isObsoleteDevice(int deviceId) {
+        switch (deviceId >> 20) {
+            case 0x100: /* TECH-US/TECH3-US */
+                return (deviceId < 0x10040000);
+            case 0x120: /* TECH3-EU */
+                return (deviceId < 0x12030000);
+            case 0x101: /* HDHR-US */
+            case 0x102: /* HDHR-T1-US */
+            case 0x103: /* HDHR3-US */
+            case 0x111: /* HDHR3-DT */
+            case 0x121: /* HDHR-EU */
+            case 0x122: /* HDHR3-EU */
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    static class HdHomeRunDiscoverDevice {
+        int mIpAddress;
+        int mDeviceType;
+        int mDeviceId;
+        int mTunerCount;
+        String mBaseUrl;
+
+        @Override
+        public boolean equals(Object other) {
+            if (this == other) {
+                return true;
+            } else if (other instanceof HdHomeRunDiscoverDevice) {
+                HdHomeRunDiscoverDevice o = (HdHomeRunDiscoverDevice) other;
+                return mIpAddress == o.mIpAddress
+                        && mDeviceType == o.mDeviceType
+                        && mDeviceId == o.mDeviceId;
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            int result = mIpAddress;
+            result = 31 * result + mDeviceType;
+            result = 31 * result + mDeviceId;
+            return result;
+        }
+    }
+
+    private static class HdHomeRunDiscoverSocket {
+        DatagramSocket mSocket;
+        boolean mDetected;
+        boolean mDiscoverPacketSent;
+        int mLocalIp;
+        int mSubnetMask;
+
+        private HdHomeRunDiscoverSocket(
+                DatagramSocket socket, boolean detected, int localIp, int subnetMask) {
+            mSocket = socket;
+            mDetected = detected;
+            mLocalIp = localIp;
+            mSubnetMask = subnetMask;
+        }
+
+        private boolean send(int targetIp, int deviceType, int deviceId) {
+            byte[] data = new byte[12];
+            ByteBuffer buffer = ByteBuffer.wrap(data);
+            buffer.put(HdHomeRunUtils.HDHOMERUN_TAG_DEVICE_TYPE);
+            buffer.put((byte) 4);
+            buffer.putInt(deviceType);
+            buffer.put(HdHomeRunUtils.HDHOMERUN_TAG_DEVICE_ID);
+            buffer.put((byte) 4);
+            buffer.putInt(deviceId);
+            data = HdHomeRunUtils.sealFrame(data, HdHomeRunUtils.HDHOMERUN_TYPE_DISCOVER_REQUEST);
+            try {
+                DatagramPacket packet =
+                        new DatagramPacket(
+                                data,
+                                data.length,
+                                HdHomeRunUtils.intToAddress(targetIp),
+                                HdHomeRunUtils.HDHOMERUN_DISCOVER_UDP_PORT);
+                mSocket.send(packet);
+                if (DEBUG) {
+                    Log.d(TAG, "Discover packet sent to: " + HdHomeRunUtils.getIpString(targetIp));
+                }
+                mDiscoverPacketSent = true;
+            } catch (IOException e) {
+                if (DEBUG) {
+                    Log.d(
+                            TAG,
+                            "Cannot send discover packet to socket("
+                                    + HdHomeRunUtils.getIpString(mLocalIp)
+                                    + ")");
+                }
+                mDiscoverPacketSent = false;
+            }
+            return mDiscoverPacketSent;
+        }
+
+        private boolean receive(HdHomeRunDiscoverDevice result) {
+            DatagramPacket packet = new DatagramPacket(new byte[3074], 3074);
+            try {
+                mSocket.setSoTimeout(HDHOMERUN_DISCOVER_RECEIVE_WAITE_TIME_MS);
+                mSocket.receive(packet);
+                if (DEBUG) Log.d(TAG, "Received packet, size: " + packet.getLength());
+            } catch (IOException e) {
+                if (DEBUG) {
+                    Log.d(
+                            TAG,
+                            "Cannot receive from socket("
+                                    + HdHomeRunUtils.getIpString(mLocalIp)
+                                    + ")");
+                }
+                return false;
+            }
+
+            Pair<Short, byte[]> data =
+                    HdHomeRunUtils.openFrame(packet.getData(), packet.getLength());
+            if (data == null
+                    || data.first == null
+                    || data.first != HdHomeRunUtils.HDHOMERUN_TYPE_DISCOVER_REPLY) {
+                if (DEBUG) Log.d(TAG, "Ill-formed packet: " + Arrays.toString(packet.getData()));
+                return false;
+            }
+            result.mIpAddress = HdHomeRunUtils.addressToInt(packet.getAddress().getAddress());
+            if (DEBUG) {
+                Log.d(TAG, "Get Device IP: " + HdHomeRunUtils.getIpString(result.mIpAddress));
+            }
+            ByteBuffer buffer = ByteBuffer.wrap(data.second);
+            while (true) {
+                Pair<Byte, byte[]> tagAndValue = HdHomeRunUtils.readTaggedValue(buffer);
+                if (tagAndValue == null) {
+                    break;
+                }
+                switch (tagAndValue.first) {
+                    case HdHomeRunUtils.HDHOMERUN_TAG_DEVICE_TYPE:
+                        if (tagAndValue.second.length != 4) {
+                            break;
+                        }
+                        result.mDeviceType = ByteBuffer.wrap(tagAndValue.second).getInt();
+                        if (DEBUG) Log.d(TAG, "Get Device Type: " + result.mDeviceType);
+                        break;
+                    case HdHomeRunUtils.HDHOMERUN_TAG_DEVICE_ID:
+                        if (tagAndValue.second.length != 4) {
+                            break;
+                        }
+                        result.mDeviceId = ByteBuffer.wrap(tagAndValue.second).getInt();
+                        if (DEBUG) Log.d(TAG, "Get Device ID: " + result.mDeviceId);
+                        break;
+                    case HdHomeRunUtils.HDHOMERUN_TAG_TUNER_COUNT:
+                        if (tagAndValue.second.length != 1) {
+                            break;
+                        }
+                        result.mTunerCount = tagAndValue.second[0];
+                        if (DEBUG) Log.d(TAG, "Get Tuner Count: " + result.mTunerCount);
+                        break;
+                    case HdHomeRunUtils.HDHOMERUN_TAG_BASE_URL:
+                        result.mBaseUrl = new String(tagAndValue.second);
+                        if (DEBUG) Log.d(TAG, "Get Base URL: " + result.mBaseUrl);
+                        break;
+                    default:
+                        break;
+                }
+            }
+            // Fixup for old firmware.
+            if (result.mTunerCount == 0) {
+                switch (result.mDeviceId >> 20) {
+                    case 0x102:
+                        result.mTunerCount = 1;
+                        break;
+                    case 0x100:
+                    case 0x101:
+                    case 0x121:
+                        result.mTunerCount = 2;
+                        break;
+                    default:
+                        break;
+                }
+            }
+            return true;
+        }
+
+        private void close() {
+            if (mSocket != null) {
+                mSocket.close();
+            }
+        }
+    }
+
+    private static class LocalIpInfo {
+        int mIpAddress;
+        int mSubnetMask;
+    }
+}
diff --git a/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunInterface.java b/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunInterface.java
new file mode 100644
index 0000000..2928aba
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunInterface.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.hdhomerun;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
+import android.util.Log;
+import com.android.tv.tuner.hdhomerun.HdHomeRunDiscover.HdHomeRunDiscoverDevice;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/** An interface class provides methods to access physical HDHomeRun devices. */
+@WorkerThread
+public class HdHomeRunInterface {
+    private static final String TAG = "HdHomeRunInterface";
+    private static final boolean DEBUG = false;
+
+    private static final int FETCH_DEVICE_NAME_TRY_NUM = 2;
+    private static final int MAX_DEVICES = 1;
+    private static final boolean DISABLE_CABLE = false;
+
+    /**
+     * Scans for HDHomeRun devices on the network.
+     *
+     * @param deviceId The target device ID we want to find, scans for all available devices if
+     *     {@code null} or the given ID cannot be found.
+     * @return A set of HDHomeRun devices
+     */
+    @NonNull
+    public static Set<HdHomeRunDevice> scanDevices(Integer deviceId) {
+        List<HdHomeRunDiscoverDevice> discoveredDevices = null;
+        if (deviceId != null) {
+            discoveredDevices =
+                    HdHomeRunUtils.findHdHomeRunDevices(
+                            0, HdHomeRunUtils.HDHOMERUN_DEVICE_TYPE_TUNER, deviceId, 1);
+            if (discoveredDevices.isEmpty()) {
+                Log.i(TAG, "Can't find device with ID: " + deviceId);
+            }
+        }
+        if (discoveredDevices == null || discoveredDevices.isEmpty()) {
+            discoveredDevices =
+                    HdHomeRunUtils.findHdHomeRunDevices(
+                            0,
+                            HdHomeRunUtils.HDHOMERUN_DEVICE_TYPE_TUNER,
+                            HdHomeRunUtils.HDHOMERUN_DEVICE_ID_WILDCARD,
+                            MAX_DEVICES);
+            if (DEBUG) Log.d(TAG, "Found " + discoveredDevices.size() + " devices");
+        }
+        Set<HdHomeRunDevice> result = new HashSet<>();
+        for (HdHomeRunDiscoverDevice discoveredDevice : discoveredDevices) {
+            String model =
+                    fetchDeviceModel(discoveredDevice.mDeviceId, discoveredDevice.mIpAddress);
+            if (model == null) {
+                Log.e(TAG, "Fetching device model failed: " + discoveredDevice.mDeviceId);
+                continue;
+            } else if (DEBUG) {
+                Log.d(TAG, "Fetch Device Model: " + model);
+            }
+            if (DISABLE_CABLE) {
+                if (model != null && model.contains("cablecard")) {
+                    // filter out CableCARD devices
+                    continue;
+                }
+            }
+            for (int i = 0; i < discoveredDevice.mTunerCount; i++) {
+                result.add(
+                        new HdHomeRunDevice(
+                                discoveredDevice.mIpAddress,
+                                discoveredDevice.mDeviceType,
+                                discoveredDevice.mDeviceId,
+                                i,
+                                model));
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Returns {@code true} if the given device IP, ID and tuner index is available for use.
+     *
+     * @param deviceId The target device ID, 0 denotes a wildcard match.
+     * @param deviceIp The target device IP.
+     * @param tunerIndex The target tuner index of the target device. This parameter is only
+     *     meaningful when the target device has multiple tuners.
+     */
+    public static boolean isDeviceAvailable(int deviceId, int deviceIp, int tunerIndex) {
+        // TODO: check the lock state for the given tuner.
+        if ((deviceId == 0) && (deviceIp == 0)) {
+            return false;
+        }
+        if (HdHomeRunUtils.isIpMulticast(deviceIp)) {
+            return false;
+        }
+        if ((deviceId == 0) || (deviceId == HdHomeRunUtils.HDHOMERUN_DEVICE_ID_WILDCARD)) {
+            try (HdHomeRunControlSocket controlSock =
+                    new HdHomeRunControlSocket(deviceId, deviceIp)) {
+                deviceId = controlSock.getDeviceId();
+            }
+        }
+        return deviceId != 0;
+    }
+
+    @Nullable
+    private static String fetchDeviceModel(int deviceId, int deviceIp) {
+        for (int i = 0; i < FETCH_DEVICE_NAME_TRY_NUM; i++) {
+            try (HdHomeRunControlSocket controlSock =
+                    new HdHomeRunControlSocket(deviceId, deviceIp)) {
+                String model = controlSock.get("/sys/model");
+                if (model != null) {
+                    return model;
+                }
+            }
+        }
+        return null;
+    }
+
+    private HdHomeRunInterface() {}
+}
diff --git a/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunTunerHal.java b/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunTunerHal.java
new file mode 100644
index 0000000..8168299
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunTunerHal.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.hdhomerun;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.compat.TvInputConstantCompat;
+import com.android.tv.tuner.api.Tuner;
+import com.android.tv.tuner.data.TunerChannel;
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+
+/** Tuner implementation for HdHomeRun */
+public class HdHomeRunTunerHal implements Tuner {
+    private static final String TAG = "HdHomeRunTunerHal";
+    private static final boolean DEBUG = false;
+
+    private static final String CABLECARD_MODEL = "cablecard";
+    private static final String ATSC_MODEL = "atsc";
+    private static final String DVBC_MODEL = "dvbc";
+    private static final String DVBT_MODEL = "dvbt";
+
+    private final HdHomeRunTunerManager mTunerManager;
+    private HdHomeRunDevice mDevice;
+    private BufferedInputStream mInputStream;
+    private HttpURLConnection mConnection;
+    private String mHttpConnectionAddress;
+    private final Context mContext;
+
+    @DeliverySystemType private int mDeliverySystemType = DELIVERY_SYSTEM_UNDEFINED;
+
+    public static final char VCHANNEL_SEPARATOR = '.';
+    public static final int CONNECTION_TIMEOUT_MS_FOR_URLCONNECTION = 3000; // 3 sec
+    public static final int READ_TIMEOUT_MS_FOR_URLCONNECTION = 10000; // 10 sec
+
+    public HdHomeRunTunerHal(Context context) {
+        mTunerManager = HdHomeRunTunerManager.getInstance();
+        mContext = context;
+    }
+
+    @Override
+    public boolean openFirstAvailable() {
+        SoftPreconditions.checkState(mDevice == null);
+        try {
+            mDevice = mTunerManager.acquireDevice(mContext);
+            if (mDevice != null) {
+                if (mDeliverySystemType == DELIVERY_SYSTEM_UNDEFINED) {
+                    mDeliverySystemType = nativeGetDeliverySystemType(getDeviceId());
+                }
+            }
+            return mDevice != null;
+        } catch (Exception e) {
+            Log.w(TAG, "Failed to open first available device", e);
+            return false;
+        }
+    }
+
+    @Override
+    public boolean isDeviceOpen() {
+        return mDevice != null;
+    }
+
+    @Override
+    public boolean isReusable() {
+        return false;
+    }
+
+    @Override
+    public long getDeviceId() {
+        return mDevice == null ? 0 : mDevice.getDeviceId();
+    }
+
+    @Override
+    public void close() throws Exception {
+        closeInputStreamAndDisconnect();
+        if (mDevice != null) {
+            mTunerManager.releaseDevice(mDevice);
+            mDevice = null;
+        }
+    }
+
+    @Override
+    public synchronized boolean tune(
+            int frequency, @ModulationType String modulation, String channelNumber) {
+        if (DEBUG) {
+            Log.d(
+                    TAG,
+                    "tune(frequency="
+                            + frequency
+                            + ", modulation="
+                            + modulation
+                            + ", channelNumber="
+                            + channelNumber
+                            + ")");
+        }
+        closeInputStreamAndDisconnect();
+        if (TextUtils.isEmpty(channelNumber)) {
+            return false;
+        }
+        channelNumber =
+                channelNumber.replace(TunerChannel.CHANNEL_NUMBER_SEPARATOR, VCHANNEL_SEPARATOR);
+        mHttpConnectionAddress = "http://" + getIpAddress() + ":5004/auto/v" + channelNumber;
+        return connectAndOpenInputStream();
+    }
+
+    private boolean connectAndOpenInputStream() {
+        URL url;
+        try {
+            url = new URL(mHttpConnectionAddress);
+        } catch (MalformedURLException e) {
+            Log.e(TAG, "Invalid address: " + mHttpConnectionAddress, e);
+            return false;
+        }
+        URLConnection connection;
+        try {
+            connection = url.openConnection();
+            connection.setConnectTimeout(CONNECTION_TIMEOUT_MS_FOR_URLCONNECTION);
+            connection.setReadTimeout(READ_TIMEOUT_MS_FOR_URLCONNECTION);
+            if (connection instanceof HttpURLConnection) {
+                mConnection = (HttpURLConnection) connection;
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "Connection failed: " + mHttpConnectionAddress, e);
+            return false;
+        }
+        try {
+            mInputStream = new BufferedInputStream(connection.getInputStream());
+        } catch (IOException e) {
+            closeInputStreamAndDisconnect();
+            Log.e(TAG, "Failed to get input stream from " + mHttpConnectionAddress, e);
+            return false;
+        }
+        if (DEBUG) Log.d(TAG, "tuning to " + mHttpConnectionAddress);
+        return true;
+    }
+
+    @Override
+    public synchronized boolean addPidFilter(int pid, @FilterType int filterType) {
+        // no-op
+        return true;
+    }
+
+    @Override
+    public synchronized void stopTune() {
+        closeInputStreamAndDisconnect();
+    }
+
+    @Override
+    public synchronized int readTsStream(byte[] javaBuffer, int javaBufferSize) {
+        if (mInputStream != null) {
+            try {
+                // Note: this call sometimes take more than 500ms, because the data is
+                // streamed through network unlike connected tuner devices.
+                return mInputStream.read(javaBuffer, 0, javaBufferSize);
+            } catch (IOException e) {
+                Log.e(TAG, "Failed to read stream", e);
+                closeInputStreamAndDisconnect();
+            }
+        }
+        if (connectAndOpenInputStream()) {
+            Log.w(TAG, "Tuned by http connection again");
+        } else {
+            Log.e(TAG, "Tuned by http connection again failed");
+        }
+        return 0;
+    }
+
+    @Override
+    public void setHasPendingTune(boolean hasPendingTune) {
+        // no-op
+    }
+
+    protected int nativeGetDeliverySystemType(long deviceId) {
+        String deviceModel = mDevice.getDeviceModel();
+        if (SoftPreconditions.checkState(!TextUtils.isEmpty(deviceModel))) {
+            if (deviceModel.contains(CABLECARD_MODEL) || deviceModel.contains(ATSC_MODEL)) {
+                return DELIVERY_SYSTEM_ATSC;
+            } else if (deviceModel.contains(DVBC_MODEL)) {
+                return DELIVERY_SYSTEM_DVBC;
+            } else if (deviceModel.contains(DVBT_MODEL)) {
+                return DELIVERY_SYSTEM_DVBT;
+            }
+        }
+        return DELIVERY_SYSTEM_UNDEFINED;
+    }
+
+    private void closeInputStreamAndDisconnect() {
+        if (mInputStream != null) {
+            try {
+                mInputStream.close();
+            } catch (IOException e) {
+                Log.e(TAG, "Failed to close input stream", e);
+            }
+            mInputStream = null;
+        }
+        if (mConnection != null) {
+            mConnection.disconnect();
+            mConnection = null;
+        }
+    }
+
+    /** Gets the number of tuners in a given HDHomeRun devices. */
+    public static int getNumberOfDevices() {
+        return HdHomeRunTunerManager.getInstance().getTunerCount();
+    }
+
+    /** Returns the IP address. */
+    public String getIpAddress() {
+        return HdHomeRunUtils.getIpString(mDevice.getIpAddress());
+    }
+
+    /**
+     * Marks the device associated to this instance as a scanned device. Scanned device has higher
+     * priority among multiple HDHomeRun devices.
+     */
+    public void markAsScannedDevice(Context context) {
+        HdHomeRunTunerManager.markAsScannedDevice(context, mDevice);
+    }
+
+    @Override
+    @DeliverySystemType
+    public int getDeliverySystemType() {
+        return Tuner.DELIVERY_SYSTEM_UNDEFINED;
+    }
+
+    @Override
+    public int getSignalStrength() {
+        return TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED;
+    }
+}
diff --git a/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunTunerHalFactory.java b/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunTunerHalFactory.java
new file mode 100644
index 0000000..6f6b186
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunTunerHalFactory.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.hdhomerun;
+
+import android.content.Context;
+import android.support.annotation.WorkerThread;
+import android.util.Pair;
+
+import com.android.tv.tuner.api.Tuner;
+import com.android.tv.tuner.api.TunerFactory;
+
+/** TunerHal factory that creates all built in tuner types. */
+public final class HdHomeRunTunerHalFactory implements TunerFactory {
+    public static final TunerFactory INSTANCE = new HdHomeRunTunerHalFactory();
+
+    private HdHomeRunTunerHalFactory() {}
+    /**
+     * Creates a TunerHal instance.
+     *
+     * @param context context for creating the TunerHal instance
+     * @return the TunerHal instance
+     */
+    @Override
+    @WorkerThread
+    public synchronized Tuner createInstance(Context context) {
+        Tuner tunerHal = null;
+        if (tunerHal == null) {
+            tunerHal = new HdHomeRunTunerHal(context);
+        }
+        return tunerHal.openFirstAvailable() ? tunerHal : null;
+    }
+
+    /**
+     * Returns if tuner input service would use built-in tuners instead of USB tuners or network
+     * tuners.
+     */
+    @Override
+    public boolean useBuiltInTuner(Context context) {
+        return false;
+    }
+
+    /** Gets the number of tuner devices currently present. */
+    @Override
+    @WorkerThread
+    public Pair<Integer, Integer> getTunerTypeAndCount(Context context) {
+        return Pair.create(Tuner.TUNER_TYPE_NETWORK, HdHomeRunTunerHal.getNumberOfDevices());
+    }
+}
diff --git a/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunTunerManager.java b/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunTunerManager.java
new file mode 100644
index 0000000..9e3ea59
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunTunerManager.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.hdhomerun;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.support.annotation.WorkerThread;
+import android.util.Log;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A class to manage tuner resources of HDHomeRun devices. It handles tuner resource acquisition and
+ * release.
+ */
+class HdHomeRunTunerManager {
+    private static final String TAG = "HdHomeRunTunerManager";
+    private static final boolean DEBUG = false;
+
+    private static final String PREF_KEY_SCANNED_DEVICE_ID = "scanned_device_id";
+
+    private static HdHomeRunTunerManager sInstance;
+
+    private final Set<HdHomeRunDevice> mHdHomeRunDevices = new HashSet<>();
+    private final Set<HdHomeRunDevice> mUsedDevices = new HashSet<>();
+
+    private HdHomeRunTunerManager() {}
+
+    /** Returns the instance of this manager. */
+    public static synchronized HdHomeRunTunerManager getInstance() {
+        if (sInstance == null) {
+            sInstance = new HdHomeRunTunerManager();
+        }
+        return sInstance;
+    }
+
+    /** Returns number of tuners. */
+    @WorkerThread
+    synchronized int getTunerCount() {
+        updateDevicesLocked(null);
+        if (DEBUG) Log.d(TAG, "getTunerCount: " + mHdHomeRunDevices.size());
+        return mHdHomeRunDevices.size();
+    }
+
+    /** Creates an HDHomeRun device. If there is no available one, returns {@code null}. */
+    @WorkerThread
+    synchronized HdHomeRunDevice acquireDevice(Context context) {
+        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
+        int scannedDeviceId = sp.getInt(PREF_KEY_SCANNED_DEVICE_ID, 0);
+        updateDevicesLocked(scannedDeviceId == 0 ? null : scannedDeviceId);
+        if (DEBUG) Log.d(TAG, "createDevice: device count = " + mHdHomeRunDevices.size());
+        HdHomeRunDevice availableDevice = null;
+        // Use the device used for scanning first since other devices might have different line-up.
+        if (scannedDeviceId != 0) {
+            for (HdHomeRunDevice device : mHdHomeRunDevices) {
+                if (!mUsedDevices.contains(device) && scannedDeviceId == device.getDeviceId()) {
+                    if (!HdHomeRunInterface.isDeviceAvailable(
+                            device.getDeviceId(), device.getIpAddress(), device.getTunerIndex())) {
+                        if (DEBUG) Log.d(TAG, "Device not available: " + device);
+                        continue;
+                    }
+                    availableDevice = device;
+                    break;
+                }
+            }
+        }
+        if (availableDevice == null) {
+            for (HdHomeRunDevice device : mHdHomeRunDevices) {
+                if (!mUsedDevices.contains(device)) {
+                    if (!HdHomeRunInterface.isDeviceAvailable(
+                            device.getDeviceId(), device.getIpAddress(), device.getTunerIndex())) {
+                        if (DEBUG) Log.d(TAG, "Device not available: " + device);
+                        continue;
+                    }
+                    availableDevice = device;
+                    break;
+                }
+            }
+        }
+        if (availableDevice != null) {
+            if (DEBUG) Log.d(TAG, "created device " + availableDevice);
+            mUsedDevices.add(availableDevice);
+            return availableDevice;
+        }
+        return null;
+    }
+
+    /** Releases a created device by {@link #acquireDevice(Context)}. */
+    synchronized void releaseDevice(HdHomeRunDevice device) {
+        if (DEBUG) Log.d(TAG, "releaseDevice: " + device);
+        mUsedDevices.remove(device);
+    }
+
+    /**
+     * Marks the device associated to this instance as a scanned device. Scanned device has higher
+     * priority among multiple HDHomeRun devices.
+     */
+    static void markAsScannedDevice(Context context, HdHomeRunDevice device) {
+        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
+        sp.edit().putInt(PREF_KEY_SCANNED_DEVICE_ID, device.getDeviceId()).apply();
+    }
+
+    private void updateDevicesLocked(Integer deviceId) {
+        mHdHomeRunDevices.clear();
+        mHdHomeRunDevices.addAll(HdHomeRunInterface.scanDevices(deviceId));
+    }
+}
diff --git a/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunUtils.java b/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunUtils.java
new file mode 100644
index 0000000..733fc96
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/hdhomerun/HdHomeRunUtils.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.hdhomerun;
+
+import android.support.annotation.NonNull;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.tv.tuner.hdhomerun.HdHomeRunDiscover.HdHomeRunDiscoverDevice;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.zip.CRC32;
+
+class HdHomeRunUtils {
+    private static final String TAG = "HdHomeRunUtils";
+    private static final boolean DEBUG = false;
+
+    static final int HDHOMERUN_DEVICE_TYPE_WILDCARD = 0xFFFFFFFF;
+    static final int HDHOMERUN_DEVICE_TYPE_TUNER = 0x00000001;
+    static final int HDHOMERUN_DEVICE_ID_WILDCARD = 0xFFFFFFFF;
+
+    static final int HDHOMERUN_DISCOVER_UDP_PORT = 65001;
+    static final int HDHOMERUN_CONTROL_TCP_PORT = 65001;
+
+    static final short HDHOMERUN_TYPE_INVALID = -1;
+    static final short HDHOMERUN_TYPE_DISCOVER_REQUEST = 0x0002;
+    static final short HDHOMERUN_TYPE_DISCOVER_REPLY = 0x0003;
+    static final short HDHOMERUN_TYPE_GETSET_REQUEST = 0x0004;
+    static final short HDHOMERUN_TYPE_GETSET_REPLY = 0x0005;
+
+    static final byte HDHOMERUN_TAG_DEVICE_TYPE = 0x01;
+    static final byte HDHOMERUN_TAG_DEVICE_ID = 0x02;
+    static final byte HDHOMERUN_TAG_GETSET_NAME = 0x03;
+    static final int HDHOMERUN_TAG_GETSET_VALUE = 0x04;
+    static final int HDHOMERUN_TAG_ERROR_MESSAGE = 0x05;
+    static final int HDHOMERUN_TAG_TUNER_COUNT = 0x10;
+    static final int HDHOMERUN_TAG_BASE_URL = 0x2A;
+
+    static final int HDHOMERUN_CONTROL_CONNECT_TIMEOUT_MS = 2500;
+    static final int HDHOMERUN_CONTROL_SEND_TIMEOUT_MS = 2500;
+    static final int HDHOMERUN_CONTROL_RECEIVE_TIMEOUT_MS = 2500;
+
+    /**
+     * Finds HDHomeRun devices with given IP, type, and ID.
+     *
+     * @param targetIp {@code 0} to find target devices with broadcasting.
+     * @param deviceType The type of target devices.
+     * @param deviceId The ID of target devices.
+     * @param maxCount Maximum number of devices should be returned.
+     */
+    @NonNull
+    static List<HdHomeRunDiscoverDevice> findHdHomeRunDevices(
+            int targetIp, int deviceType, int deviceId, int maxCount) {
+        if (isIpMulticast(targetIp)) {
+            if (DEBUG) Log.d(TAG, "Target IP cannot be multicast IP.");
+            return Collections.emptyList();
+        }
+        try {
+            HdHomeRunDiscover ds = HdHomeRunDiscover.create();
+            if (ds == null) {
+                if (DEBUG) Log.d(TAG, "Cannot create discover object.");
+                return Collections.emptyList();
+            }
+            List<HdHomeRunDiscoverDevice> result =
+                    ds.findDevices(targetIp, deviceType, deviceId, maxCount);
+            ds.close();
+            return result;
+        } catch (Exception e) {
+            Log.w(TAG, "Failed to find HdHomeRun Devices", e);
+            return Collections.emptyList();
+        }
+    }
+
+    /** Returns {@code true} if the given IP is a multi-cast IP. */
+    static boolean isIpMulticast(long ip) {
+        return (ip >= 0xE0000000) && (ip < 0xF0000000);
+    }
+
+    /** Translates a {@code byte[]} address to its integer representation. */
+    static int addressToInt(byte[] address) {
+        return ByteBuffer.wrap(address).order(ByteOrder.LITTLE_ENDIAN).getInt();
+    }
+
+    /** Translates an {@code int} address to a corresponding {@link InetAddress}. */
+    static InetAddress intToAddress(int address) throws UnknownHostException {
+        return InetAddress.getByAddress(
+                ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(address).array());
+    }
+
+    /** Gets {@link String} representation of an {@code int} address. */
+    static String getIpString(int ip) {
+        return String.format(
+                "%d.%d.%d.%d", (ip & 0xff), (ip >> 8 & 0xff), (ip >> 16 & 0xff), (ip >> 24 & 0xff));
+    }
+
+    /**
+     * Opens the packet returned from HDHomeRun devices to acquire the real content and verify it.
+     */
+    static Pair<Short, byte[]> openFrame(byte[] data, int length) {
+        if (length < 4) {
+            return null;
+        }
+        ByteBuffer buffer = ByteBuffer.wrap(data);
+        short resultType = buffer.getShort();
+        int dataLength = buffer.getShort() & 0xffff;
+
+        if (dataLength + 8 > length) {
+            // Not finished yet.
+            return null;
+        }
+        byte[] result = new byte[dataLength];
+        buffer.get(result);
+        byte[] calculatedCrc = getCrcFromBytes(Arrays.copyOfRange(data, 0, dataLength + 4));
+        byte[] packetCrc = new byte[4];
+        buffer.get(packetCrc);
+
+        if (!Arrays.equals(calculatedCrc, packetCrc)) {
+            return Pair.create(HDHOMERUN_TYPE_INVALID, null);
+        }
+
+        return Pair.create(resultType, result);
+    }
+
+    /** Seals the contents in a packet to send to HDHomeRun devices. */
+    static byte[] sealFrame(byte[] data, short frameType) {
+        byte[] result = new byte[data.length + 8];
+        ByteBuffer buffer = ByteBuffer.wrap(result);
+        buffer.putShort(frameType);
+        buffer.putShort((short) data.length);
+        buffer.put(data);
+        buffer.put(getCrcFromBytes(Arrays.copyOfRange(result, 0, data.length + 4)));
+        return result;
+    }
+
+    /** Reads a (tag, value) pair from packets returned from HDHomeRun devices. */
+    static Pair<Byte, byte[]> readTaggedValue(ByteBuffer buffer) {
+        try {
+            Byte tag = buffer.get();
+            byte[] value = readVarLength(buffer);
+            return Pair.create(tag, value);
+        } catch (BufferUnderflowException e) {
+            return null;
+        }
+    }
+
+    private static byte[] readVarLength(ByteBuffer buffer) {
+        short length;
+        Byte lengthByte1 = buffer.get();
+        if ((lengthByte1 & 0x80) != 0) {
+            length = buffer.get();
+            length = (short) ((length << 7) + (lengthByte1 & 0x7F));
+        } else {
+            length = lengthByte1;
+        }
+        byte[] result = new byte[length];
+        buffer.get(result);
+        return result;
+    }
+
+    private static byte[] getCrcFromBytes(byte[] data) {
+        CRC32 crc32 = new CRC32();
+        crc32.update(data);
+        long crc = crc32.getValue();
+        byte[] result = new byte[4];
+        for (int offset = 0; offset < 4; offset++) {
+            result[offset] = (byte) (crc & 0xFF);
+            crc >>= 8;
+        }
+        return result;
+    }
+
+    private HdHomeRunUtils() {}
+}
diff --git a/tuner/src/com/android/tv/tuner/modules/TunerModule.java b/tuner/src/com/android/tv/tuner/modules/TunerModule.java
index 4843f38..bf40fbe 100644
--- a/tuner/src/com/android/tv/tuner/modules/TunerModule.java
+++ b/tuner/src/com/android/tv/tuner/modules/TunerModule.java
@@ -16,8 +16,49 @@
 package com.android.tv.tuner.modules;
 
 import com.android.tv.tuner.source.TunerSourceModule;
+import com.android.tv.tuner.tvinput.TunerRecordingSessionFactoryImpl;
+import com.android.tv.tuner.tvinput.TunerRecordingSessionWorker;
+import com.android.tv.tuner.tvinput.TunerRecordingSessionWorkerFactory;
+import com.android.tv.tuner.tvinput.TunerSessionExoV2Factory;
+import com.android.tv.tuner.tvinput.TunerSessionV1Factory;
+import com.android.tv.tuner.tvinput.TunerSessionWorker;
+import com.android.tv.tuner.tvinput.TunerSessionWorkerExoV2;
+import com.android.tv.tuner.tvinput.TunerSessionWorkerExoV2Factory;
+import com.android.tv.tuner.tvinput.TunerSessionWorkerFactory;
+import com.android.tv.tuner.tvinput.factory.TunerRecordingSessionFactory;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactory;
+
+import dagger.Binds;
 import dagger.Module;
+import dagger.Provides;
+
+import com.android.tv.common.flags.TunerFlags;
 
 /** Dagger module for TV Tuners. */
 @Module(includes = {TunerSingletonsModule.class, TunerSourceModule.class})
-public class TunerModule {}
+public abstract class TunerModule {
+
+    @Provides
+    static TunerSessionFactory tunerSessionFactory(
+            TunerFlags tunerFlags,
+            TunerSessionV1Factory tunerSessionFactory,
+            TunerSessionExoV2Factory tunerSessionExoV2Factory) {
+        return tunerFlags.useExoplayerV2() ? tunerSessionExoV2Factory : tunerSessionFactory;
+    }
+
+    @Binds
+    abstract TunerRecordingSessionWorker.Factory tunerRecordingSessionWorkerFactory(
+            TunerRecordingSessionWorkerFactory tunerRecordingSessionWorkerFactory);
+
+    @Binds
+    abstract TunerSessionWorker.Factory tunerSessionWorkerFactory(
+            TunerSessionWorkerFactory tunerSessionWorkerFactory);
+
+    @Binds
+    abstract TunerSessionWorkerExoV2.Factory tunerSessionWorkerExoV2Factory(
+            TunerSessionWorkerExoV2Factory tunerSessionWorkerExoV2Factory);
+
+    @Binds
+    abstract TunerRecordingSessionFactory tunerRecordingSessionFactory(
+            TunerRecordingSessionFactoryImpl impl);
+}
diff --git a/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java b/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java
index 44f689b..0502690 100644
--- a/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java
+++ b/tuner/src/com/android/tv/tuner/setup/BaseTunerSetupActivity.java
@@ -75,11 +75,14 @@
         R.raw.ut_kr_cable_standard_center_frequencies_qam256,
         R.raw.ut_kr_all,
         R.raw.ut_kr_dev_cj_cable_center_frequencies_qam256,
-        R.raw.ut_euro_dvbt_all,
-        R.raw.ut_euro_dvbt_all,
         R.raw.ut_euro_dvbt_all
+        /* these two resource files are obsolete and removed, so comment them out
+        R.raw.ut_euro_all,
+        R.raw.ut_euro_all */
     };
 
+    protected final String mInputId;
+
     protected ScanFragment mLastScanFragment;
     protected Integer mTunerType;
     protected boolean mNeedToShowPostalCodeFragment;
@@ -90,6 +93,10 @@
 
     private TunerHalCreator mTunerHalCreator;
 
+    protected BaseTunerSetupActivity(String mInputId) {
+        this.mInputId = mInputId;
+    }
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         if (DEBUG) {
@@ -222,6 +229,7 @@
                 args1.putInt(
                         ScanFragment.EXTRA_FOR_CHANNEL_SCAN_FILE, CHANNEL_MAP_SCAN_FILE[actionId]);
                 args1.putInt(KEY_TUNER_TYPE, mTunerType);
+                args1.putString(ScanFragment.EXTRA_FOR_INPUT_ID, mInputId);
                 mLastScanFragment.setArguments(args1);
                 showFragment(mLastScanFragment, true);
                 return true;
diff --git a/tuner/src/com/android/tv/tuner/setup/ChannelScanFileParser.java b/tuner/src/com/android/tv/tuner/setup/ChannelScanFileParser.java
index 43c584e..2e78270 100644
--- a/tuner/src/com/android/tv/tuner/setup/ChannelScanFileParser.java
+++ b/tuner/src/com/android/tv/tuner/setup/ChannelScanFileParser.java
@@ -56,6 +56,7 @@
                 }
                 scanChannelList.add(
                         ScanChannel.forTuner(
+                                tokens[0],
                                 Integer.parseInt(tokens[1]),
                                 tokens[2],
                                 tokens.length == 4 ? Integer.parseInt(tokens[3]) : null));
diff --git a/tuner/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java b/tuner/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java
index ebe4e41..db29742 100644
--- a/tuner/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java
+++ b/tuner/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java
@@ -18,14 +18,12 @@
 
 import android.os.Bundle;
 import android.support.annotation.NonNull;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
-import com.android.tv.common.BuildConfig;
+import androidx.leanback.widget.GuidanceStylist.Guidance;
+import androidx.leanback.widget.GuidedAction;
 import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
 import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
 import com.android.tv.tuner.R;
 import java.util.List;
-import java.util.TimeZone;
 
 /** A fragment for connection type selection. */
 public class ConnectionTypeFragment extends SetupMultiPaneFragment {
@@ -67,7 +65,6 @@
 
     /** The content fragment of {@link ConnectionTypeFragment}. */
     public static class ContentFragment extends SetupGuidedStepFragment {
-
         @NonNull
         @Override
         public Guidance onCreateGuidance(Bundle savedInstanceState) {
diff --git a/tuner/src/com/android/tv/tuner/setup/LineupFragment.java b/tuner/src/com/android/tv/tuner/setup/LineupFragment.java
index 41f755d..224237d 100644
--- a/tuner/src/com/android/tv/tuner/setup/LineupFragment.java
+++ b/tuner/src/com/android/tv/tuner/setup/LineupFragment.java
@@ -20,8 +20,8 @@
 import android.os.Bundle;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
+import androidx.leanback.widget.GuidanceStylist.Guidance;
+import androidx.leanback.widget.GuidedAction;
 import android.util.Log;
 import android.view.View;
 import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
diff --git a/tuner/src/com/android/tv/tuner/setup/LiveTvTunerSetupActivity.java b/tuner/src/com/android/tv/tuner/setup/LiveTvTunerSetupActivity.java
deleted file mode 100644
index 741edc7..0000000
--- a/tuner/src/com/android/tv/tuner/setup/LiveTvTunerSetupActivity.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tv.tuner.setup;
-
-import android.app.FragmentManager;
-import android.content.pm.PackageManager;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.view.KeyEvent;
-import com.android.tv.common.util.PostalCodeUtils;
-import dagger.android.ContributesAndroidInjector;
-
-/** An activity that serves tuner setup process. */
-public class LiveTvTunerSetupActivity extends BaseTunerSetupActivity {
-    private static final String TAG = "LiveTvTunerSetupActivity";
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        // TODO(shubang): use LocationFragment
-        if (checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION)
-                != PackageManager.PERMISSION_GRANTED) {
-            // No need to check the request result.
-            requestPermissions(
-                    new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION},
-                    PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION);
-        }
-    }
-
-    @Override
-    protected void executeGetTunerTypeAndCountAsyncTask() {
-        new AsyncTask<Void, Void, Integer>() {
-            @Override
-            protected Integer doInBackground(Void... arg0) {
-                return mTunerFactory.getTunerTypeAndCount(LiveTvTunerSetupActivity.this).first;
-            }
-
-            @Override
-            protected void onPostExecute(Integer result) {
-                if (!LiveTvTunerSetupActivity.this.isDestroyed()) {
-                    mTunerType = result;
-                    if (result == null) {
-                        finish();
-                    } else if (!mActivityStopped) {
-                        showInitialFragment();
-                    } else {
-                        mPendingShowInitialFragment = true;
-                    }
-                }
-            }
-        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
-    }
-
-    @Override
-    public boolean onKeyUp(int keyCode, KeyEvent event) {
-        if (keyCode == KeyEvent.KEYCODE_BACK) {
-            FragmentManager manager = getFragmentManager();
-            int count = manager.getBackStackEntryCount();
-            if (count > 0) {
-                String lastTag = manager.getBackStackEntryAt(count - 1).getName();
-                if (ScanResultFragment.class.getCanonicalName().equals(lastTag) && count >= 2) {
-                    String secondLastTag = manager.getBackStackEntryAt(count - 2).getName();
-                    if (ScanFragment.class.getCanonicalName().equals(secondLastTag)) {
-                        // Pops fragment including ScanFragment.
-                        manager.popBackStack(
-                                secondLastTag, FragmentManager.POP_BACK_STACK_INCLUSIVE);
-                        return true;
-                    }
-                } else if (ScanFragment.class.getCanonicalName().equals(lastTag)) {
-                    mLastScanFragment.finishScan(true);
-                    return true;
-                }
-            }
-        }
-        return super.onKeyUp(keyCode, event);
-    }
-
-    @Override
-    public void onRequestPermissionsResult(
-            int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
-        if (requestCode == PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION) {
-            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
-                try {
-                    // Updating postal code takes time, therefore we should update postal code
-                    // right after the permission is granted, so that the subsequent operations,
-                    // especially EPG fetcher, could get the newly updated postal code.
-                    PostalCodeUtils.updatePostalCode(this);
-                } catch (Exception e) {
-                    // Do nothing
-                }
-            }
-        }
-    }
-
-    /**
-     * Exports {@link LiveTvTunerSetupActivity} for Dagger codegen to create the appropriate
-     * injector.
-     */
-    @dagger.Module
-    public abstract static class Module {
-        @ContributesAndroidInjector
-        abstract LiveTvTunerSetupActivity contributeLiveTvTunerSetupActivityInjector();
-    }
-}
diff --git a/tuner/src/com/android/tv/tuner/setup/LocationFragment.java b/tuner/src/com/android/tv/tuner/setup/LocationFragment.java
index 1234ae2..f950405 100644
--- a/tuner/src/com/android/tv/tuner/setup/LocationFragment.java
+++ b/tuner/src/com/android/tv/tuner/setup/LocationFragment.java
@@ -23,16 +23,14 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.support.annotation.NonNull;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
 import android.util.Log;
-
+import androidx.leanback.widget.GuidanceStylist.Guidance;
+import androidx.leanback.widget.GuidedAction;
 import com.android.tv.common.ui.setup.SetupActionHelper;
 import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
 import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
 import com.android.tv.common.util.LocationUtils;
 import com.android.tv.tuner.R;
-
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
@@ -40,7 +38,7 @@
 /** A fragment shows the rationale of location permission */
 public class LocationFragment extends SetupMultiPaneFragment {
     private static final String TAG = "com.android.tv.tuner.setup.LocationFragment";
-    private static final boolean DEBUG = true;
+    private static final boolean DEBUG = false;
 
     public static final String ACTION_CATEGORY = "com.android.tv.tuner.setup.LocationFragment";
     public static final String KEY_POSTAL_CODE = "key_postal_code";
@@ -79,8 +77,7 @@
                 () -> {
                     synchronized (mPostalCodeLock) {
                         if (DEBUG) {
-                            Log.d(TAG,
-                                    "get location timeout. mPostalCode=" + mPostalCode);
+                            Log.d(TAG, "get location timeout. mPostalCode=" + mPostalCode);
                         }
                         if (mPostalCode == null) {
                             // timeout. setup activity will get null postal code
@@ -121,8 +118,7 @@
                             .id(ACTION_GETTING_LOCATION)
                             .title(getString(R.string.location_choices_getting_location))
                             .focusable(false)
-                            .build()
-            );
+                            .build());
         }
 
         @Override
@@ -147,8 +143,8 @@
         }
 
         @Override
-        public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
-                @NonNull int[] grantResults) {
+        public void onRequestPermissionsResult(
+                int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
             if (requestCode == PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION) {
                 if (grantResults.length > 0
                         && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
diff --git a/tuner/src/com/android/tv/tuner/setup/PostalCodeFragment.java b/tuner/src/com/android/tv/tuner/setup/PostalCodeFragment.java
index 5224797..f9ea167 100644
--- a/tuner/src/com/android/tv/tuner/setup/PostalCodeFragment.java
+++ b/tuner/src/com/android/tv/tuner/setup/PostalCodeFragment.java
@@ -18,9 +18,9 @@
 
 import android.os.Bundle;
 import android.support.annotation.NonNull;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
-import android.support.v17.leanback.widget.GuidedActionsStylist;
+import androidx.leanback.widget.GuidanceStylist.Guidance;
+import androidx.leanback.widget.GuidedAction;
+import androidx.leanback.widget.GuidedActionsStylist;
 import android.text.InputFilter;
 import android.text.InputFilter.AllCaps;
 import android.view.View;
diff --git a/tuner/src/com/android/tv/tuner/setup/ScanFragment.java b/tuner/src/com/android/tv/tuner/setup/ScanFragment.java
index 7d59284..87a79e3 100644
--- a/tuner/src/com/android/tv/tuner/setup/ScanFragment.java
+++ b/tuner/src/com/android/tv/tuner/setup/ScanFragment.java
@@ -40,11 +40,9 @@
 import com.android.tv.tuner.R;
 import com.android.tv.tuner.api.ScanChannel;
 import com.android.tv.tuner.api.Tuner;
+import com.android.tv.tuner.data.Channel.TunerType;
 import com.android.tv.tuner.data.PsipData;
 import com.android.tv.tuner.data.TunerChannel;
-import com.android.tv.tuner.data.nano.Channel;
-
-
 import com.android.tv.tuner.prefs.TunerPreferences;
 import com.android.tv.tuner.source.FileTsStreamer;
 import com.android.tv.tuner.source.TsDataSource;
@@ -74,7 +72,13 @@
     public static final int ACTION_FINISH = 2;
 
     public static final String EXTRA_FOR_CHANNEL_SCAN_FILE = "scan_file_choice";
+    public static final String EXTRA_FOR_INPUT_ID = "input_id";
     public static final String KEY_CHANNEL_NUMBERS = "channel_numbers";
+
+    // Allows adding audio-only channels (CJ music channel) for which VCT is not present.
+    private static final boolean ADD_CJ_MUSIC_CHANNELS = false;
+    private static final int CJ_MUSIC_CHANNEL_FREQUENCY = 585000000;
+
     private static final long CHANNEL_SCAN_SHOW_DELAY_MS = 10000;
     private static final long CHANNEL_SCAN_PERIOD_MS = 4000;
     private static final long SHOW_PROGRESS_DIALOG_DELAY_MS = 300;
@@ -99,8 +103,6 @@
         if (DEBUG) Log.d(TAG, "onCreateView");
         View view = super.onCreateView(inflater, container, savedInstanceState);
         mChannelNumbers = new ArrayList<>();
-        mChannelDataManager = new ChannelDataManager(getActivity().getApplicationContext());
-        mChannelDataManager.checkDataVersion(getActivity());
         mAdapter = new ChannelAdapter();
         mProgressBar = (ProgressBar) view.findViewById(R.id.tune_progress);
         mScanningMessage = (TextView) view.findViewById(R.id.tune_description);
@@ -122,8 +124,6 @@
                 });
         Bundle args = getArguments();
         int tunerType = (args == null ? 0 : args.getInt(BaseTunerSetupActivity.KEY_TUNER_TYPE, 0));
-        // TODO: Handle the case when the fragment is restored.
-        startScan(args == null ? 0 : args.getInt(EXTRA_FOR_CHANNEL_SCAN_FILE, 0));
         TextView scanTitleView = (TextView) view.findViewById(R.id.tune_title);
         switch (tunerType) {
             case Tuner.TUNER_TYPE_USB:
@@ -139,6 +139,28 @@
     }
 
     @Override
+    public void onStart() {
+        super.onStart();
+        Bundle args = getArguments();
+        String inputId = args == null ? null : args.getString(ScanFragment.EXTRA_FOR_INPUT_ID);
+        if (inputId == null) {
+            Log.w(TAG, "No input ID, stopping setup activity.");
+            getActivity().finish();
+        }
+
+        mChannelDataManager = new ChannelDataManager(getContext().getApplicationContext(), inputId);
+        mChannelDataManager.checkDataVersion(getActivity());
+    }
+
+    @Override
+    public void onStop() {
+        if (mChannelDataManager != null) {
+            mChannelDataManager.release();
+        }
+        super.onStop();
+    }
+
+    @Override
     protected int getLayoutResourceId() {
         return R.layout.ut_channel_scan;
     }
@@ -154,6 +176,13 @@
     }
 
     @Override
+    public void onResume() {
+        Bundle args = getArguments();
+        startScan(args == null ? 0 : args.getInt(EXTRA_FOR_CHANNEL_SCAN_FILE, 0));
+        super.onResume();
+    }
+
+    @Override
     public void onPause() {
         Log.d(TAG, "onPause");
         if (mChannelScanTask != null) {
@@ -250,6 +279,7 @@
 
         private final Activity mActivity;
         private final int mChannelMapId;
+// AOSP_Comment_Out         private final com.android.tv.tuner.hdhomerun.HdHomeRunTunerHal mNetworkTuner;
         private final TsStreamer mScanTsStreamer;
         private final TsStreamer mFileTsStreamer;
         private final ConditionVariable mConditionStopped;
@@ -270,6 +300,13 @@
                 if (hal == null) {
                     throw new RuntimeException("Failed to open a DVB device");
                 }
+                /* Begin_AOSP_Comment_Out
+                if (hal instanceof com.android.tv.tuner.hdhomerun.HdHomeRunTunerHal) {
+                    mNetworkTuner = (com.android.tv.tuner.hdhomerun.HdHomeRunTunerHal) hal;
+                } else {
+                    mNetworkTuner = null;
+                }
+                End_AOSP_Comment_Out */
                 mScanTsStreamer = new TunerTsStreamer(hal, this);
             }
             mFileTsStreamer = SCAN_LOCAL_STREAMS ? new FileTsStreamer(this, mActivity) : null;
@@ -314,6 +351,18 @@
 
         @Override
         protected Void doInBackground(Void... params) {
+            /* Begin_AOSP_Comment_Out
+            if (mNetworkTuner != null) {
+                mChannelDataManager.notifyScanStarted();
+                com.android.tv.tuner.hdhomerun.HdHomeRunChannelScan hdHomeRunChannelScan =
+                        new com.android.tv.tuner.hdhomerun.HdHomeRunChannelScan(
+                                mActivity.getApplicationContext(), this, mNetworkTuner);
+                hdHomeRunChannelScan.scan(mConditionStopped);
+                mChannelDataManager.notifyScanCompleted();
+                publishProgress(MAX_PROGRESS);
+                return null;
+            }
+            End_AOSP_Comment_Out */
             mScanChannelList.clear();
             if (SCAN_LOCAL_STREAMS) {
                 FileTsStreamer.addLocalStreamFiles(mScanChannelList);
@@ -376,6 +425,10 @@
                                 e);
                     }
                     streamer.stopStream();
+
+                    if (ADD_CJ_MUSIC_CHANNELS) {
+                        addCjMusicChannel(frequency, modulation);
+                    }
                     addChannelsWithoutVct(scanChannel);
                     if (System.currentTimeMillis() > startMs + CHANNEL_SCAN_SHOW_DELAY_MS
                             && !mChannelListVisible) {
@@ -394,6 +447,24 @@
             if (DEBUG) Log.i(TAG, "Channel scan ended");
         }
 
+        private void addCjMusicChannel(int frequency, String modulation) {
+            if (frequency == CJ_MUSIC_CHANNEL_FREQUENCY
+                    && mChannelMapId == R.raw.ut_kr_dev_cj_cable_center_frequencies_qam256) {
+                List<TunerChannel> incompleteChannels =
+                        mScanTsStreamer instanceof TunerTsStreamer
+                                ? ((TunerTsStreamer) mScanTsStreamer).getMalFormedChannels()
+                                : new ArrayList<>();
+                for (TunerChannel tunerChannel : incompleteChannels) {
+                    if ((tunerChannel.getVideoPid() == TunerChannel.INVALID_PID)
+                            && (tunerChannel.getAudioPid() != TunerChannel.INVALID_PID)) {
+                        tunerChannel.setFrequency(frequency);
+                        tunerChannel.setModulation(modulation);
+                        onChannelDetected(tunerChannel, true);
+                    }
+                }
+            }
+        }
+
         private void addChannelsWithoutVct(ScanChannel scanChannel) {
             if (scanChannel.radioFrequencyNumber == null
                     || !(mScanTsStreamer instanceof TunerTsStreamer)) {
@@ -403,6 +474,7 @@
                     ((TunerTsStreamer) mScanTsStreamer).getMalFormedChannels()) {
                 if ((tunerChannel.getVideoPid() != TunerChannel.INVALID_PID)
                         && (tunerChannel.getAudioPid() != TunerChannel.INVALID_PID)) {
+                    tunerChannel.setDeliverySystemType(scanChannel.deliverySystemType);
                     tunerChannel.setFrequency(scanChannel.frequency);
                     tunerChannel.setModulation(scanChannel.modulation);
                     tunerChannel.setShortName(
@@ -420,9 +492,9 @@
 
         private TsStreamer getStreamer(int type) {
             switch (type) {
-                case Channel.TunerType.TYPE_TUNER:
+                case TunerType.TYPE_TUNER_VALUE:
                     return mScanTsStreamer;
-                case Channel.TunerType.TYPE_FILE:
+                case TunerType.TYPE_FILE_VALUE:
                     return mFileTsStreamer;
                 default:
                     return null;
diff --git a/tuner/src/com/android/tv/tuner/setup/ScanResultFragment.java b/tuner/src/com/android/tv/tuner/setup/ScanResultFragment.java
index bd3f9ad..01bcc9f 100644
--- a/tuner/src/com/android/tv/tuner/setup/ScanResultFragment.java
+++ b/tuner/src/com/android/tv/tuner/setup/ScanResultFragment.java
@@ -20,8 +20,8 @@
 import android.content.res.Resources;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
+import androidx.leanback.widget.GuidanceStylist.Guidance;
+import androidx.leanback.widget.GuidedAction;
 import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
 import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
 import com.android.tv.tuner.R;
diff --git a/tuner/src/com/android/tv/tuner/setup/WelcomeFragment.java b/tuner/src/com/android/tv/tuner/setup/WelcomeFragment.java
index 2a414df..dfa994b 100644
--- a/tuner/src/com/android/tv/tuner/setup/WelcomeFragment.java
+++ b/tuner/src/com/android/tv/tuner/setup/WelcomeFragment.java
@@ -19,8 +19,8 @@
 import android.os.Bundle;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
+import androidx.leanback.widget.GuidanceStylist.Guidance;
+import androidx.leanback.widget.GuidedAction;
 import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
 import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
 import com.android.tv.tuner.R;
diff --git a/tuner/src/com/android/tv/tuner/singletons/TunerSingletons.java b/tuner/src/com/android/tv/tuner/singletons/TunerSingletons.java
index 48b17dc..dfe9005 100644
--- a/tuner/src/com/android/tv/tuner/singletons/TunerSingletons.java
+++ b/tuner/src/com/android/tv/tuner/singletons/TunerSingletons.java
@@ -18,4 +18,17 @@
 import com.android.tv.common.singletons.HasTvInputId;
 
 /** Singletons used in tuner applications */
-public interface TunerSingletons extends HasTvInputId {}
+public interface TunerSingletons extends HasTvInputId {
+
+    /*
+     * Do not add any new methods here.
+     *
+     * To move a getter to Injection.
+     *  1. Make a type injectable @Singleton.
+     *  2. Mark the getter here as deprecated.
+     *  3. Lazily inject the object in TvApplication.
+     *  4. Move easy usages of getters to injection instead.
+     *  5. Delete the method when all usages are migrated.
+     */
+
+}
diff --git a/tuner/src/com/android/tv/tuner/source/FileSourceEventDetector.java b/tuner/src/com/android/tv/tuner/source/FileSourceEventDetector.java
index 85932c8..5ee897b 100644
--- a/tuner/src/com/android/tv/tuner/source/FileSourceEventDetector.java
+++ b/tuner/src/com/android/tv/tuner/source/FileSourceEventDetector.java
@@ -24,9 +24,9 @@
 import com.android.tv.tuner.data.PsipData.EitItem;
 import com.android.tv.tuner.data.PsipData.SdtItem;
 import com.android.tv.tuner.data.PsipData.VctItem;
+import com.android.tv.tuner.data.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
 import com.android.tv.tuner.data.TunerChannel;
-import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
 import com.android.tv.tuner.ts.EventDetector.EventListener;
 import com.android.tv.tuner.ts.TsParser;
 import java.util.ArrayList;
diff --git a/tuner/src/com/android/tv/tuner/source/FileTsStreamer.java b/tuner/src/com/android/tv/tuner/source/FileTsStreamer.java
index 99d37e3..15f3458 100644
--- a/tuner/src/com/android/tv/tuner/source/FileTsStreamer.java
+++ b/tuner/src/com/android/tv/tuner/source/FileTsStreamer.java
@@ -17,7 +17,9 @@
 package com.android.tv.tuner.source;
 
 import android.content.Context;
+import android.net.Uri;
 import android.os.Environment;
+import android.support.annotation.Nullable;
 import android.util.Log;
 import android.util.SparseBooleanArray;
 import com.android.tv.common.SoftPreconditions;
@@ -26,8 +28,8 @@
 import com.android.tv.tuner.features.TunerFeatures;
 import com.android.tv.tuner.ts.EventDetector.EventListener;
 import com.android.tv.tuner.ts.TsParser;
-import com.google.android.exoplayer.C;
-import com.google.android.exoplayer.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.TransferListener;
 import java.io.BufferedInputStream;
 import java.io.File;
 import java.io.FileInputStream;
@@ -71,6 +73,7 @@
     public static class FileDataSource extends TsDataSource {
         private final FileTsStreamer mTsStreamer;
         private final AtomicLong mLastReadPosition = new AtomicLong(0);
+        private Uri mUri;
         private long mStartBufferedPosition;
 
         private FileDataSource(FileTsStreamer tsStreamer) {
@@ -96,9 +99,10 @@
         }
 
         @Override
-        public long open(DataSpec dataSpec) throws IOException {
+        public long open(DataSpec dataSpec) {
+            mUri = dataSpec.uri;
             mLastReadPosition.set(0);
-            return C.LENGTH_UNBOUNDED;
+            return com.google.android.exoplayer2.C.LENGTH_UNSET;
         }
 
         @Override
@@ -117,6 +121,19 @@
             }
             return ret;
         }
+
+        // ExoPlayer V2 DataSource implementation.
+
+        @Override
+        public void addTransferListener(TransferListener transferListener) {
+            // TODO: Implement to support metrics collection.
+        }
+
+        @Nullable
+        @Override
+        public Uri getUri() {
+            return mUri;
+        }
     }
 
     /**
diff --git a/tuner/src/com/android/tv/tuner/source/TsDataSource.java b/tuner/src/com/android/tv/tuner/source/TsDataSource.java
index cf3c25d..18f4458 100644
--- a/tuner/src/com/android/tv/tuner/source/TsDataSource.java
+++ b/tuner/src/com/android/tv/tuner/source/TsDataSource.java
@@ -17,7 +17,7 @@
 package com.android.tv.tuner.source;
 
 import com.android.tv.common.compat.TvInputConstantCompat;
-import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSource;
 
 /** {@link DataSource} for MPEG-TS stream, which will be used by {@link TsExtractor}. */
 public abstract class TsDataSource implements DataSource {
diff --git a/tuner/src/com/android/tv/tuner/source/TsDataSourceManager.java b/tuner/src/com/android/tv/tuner/source/TsDataSourceManager.java
index 28756a9..3c00b5c 100644
--- a/tuner/src/com/android/tv/tuner/source/TsDataSourceManager.java
+++ b/tuner/src/com/android/tv/tuner/source/TsDataSourceManager.java
@@ -19,8 +19,8 @@
 import android.content.Context;
 import android.support.annotation.VisibleForTesting;
 import com.android.tv.tuner.api.Tuner;
+import com.android.tv.tuner.data.Channel;
 import com.android.tv.tuner.data.TunerChannel;
-import com.android.tv.tuner.data.nano.Channel;
 import com.android.tv.tuner.ts.EventDetector.EventListener;
 import com.google.auto.factory.AutoFactory;
 import com.google.auto.factory.Provided;
diff --git a/tuner/src/com/android/tv/tuner/source/TunerTsStreamer.java b/tuner/src/com/android/tv/tuner/source/TunerTsStreamer.java
index 9e68c91..19058c8 100644
--- a/tuner/src/com/android/tv/tuner/source/TunerTsStreamer.java
+++ b/tuner/src/com/android/tv/tuner/source/TunerTsStreamer.java
@@ -17,8 +17,11 @@
 package com.android.tv.tuner.source;
 
 import android.content.Context;
+import android.net.Uri;
+import android.support.annotation.Nullable;
 import android.util.Log;
 import android.util.Pair;
+
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.tuner.api.ScanChannel;
 import com.android.tv.tuner.api.Tuner;
@@ -26,8 +29,10 @@
 import com.android.tv.tuner.prefs.TunerPreferences;
 import com.android.tv.tuner.ts.EventDetector;
 import com.android.tv.tuner.ts.EventDetector.EventListener;
-import com.google.android.exoplayer.C;
-import com.google.android.exoplayer.upstream.DataSpec;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.TransferListener;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
@@ -66,6 +71,7 @@
         private final TunerTsStreamer mTsStreamer;
         private final AtomicLong mLastReadPosition = new AtomicLong(0);
         private long mStartBufferedPosition;
+        private Uri mUri;
 
         private TunerDataSource(TunerTsStreamer tsStreamer) {
             mTsStreamer = tsStreamer;
@@ -90,13 +96,16 @@
         }
 
         @Override
-        public long open(DataSpec dataSpec) throws IOException {
+        public long open(DataSpec dataSpec) {
+            mUri = dataSpec.uri;
             mLastReadPosition.set(0);
-            return C.LENGTH_UNBOUNDED;
+            return C.LENGTH_UNSET;
         }
 
         @Override
-        public void close() {}
+        public void close() {
+            mUri = null;
+        }
 
         @Override
         public int read(byte[] buffer, int offset, int readLength) throws IOException {
@@ -126,6 +135,18 @@
         public int getSignalStrength() {
             return mTsStreamer.getSignalStrength();
         }
+
+        @Override
+        public void addTransferListener(TransferListener transferListener) {
+            // TODO: Implement to support metrics collection.
+        }
+
+        @Nullable
+        @Override
+        public Uri getUri() {
+            return mUri;
+        }
+
     }
     /**
      * Creates {@link TsStreamer} for playing or recording the specified channel.
@@ -152,7 +173,8 @@
     @Override
     public boolean startStream(TunerChannel channel) {
         if (mTunerHal.tune(
-                channel.getFrequency(), channel.getModulation(), channel.getDisplayNumber(false))) {
+                channel.getDeliverySystemType().getNumber(), channel.getFrequency(),
+                channel.getModulation(), channel.getDisplayNumber(false))) {
             if (channel.hasVideo()) {
                 mTunerHal.addPidFilter(channel.getVideoPid(), Tuner.FILTER_TYPE_VIDEO);
             }
@@ -170,6 +192,7 @@
             mTunerHal.addPidFilter(channel.getPcrPid(), Tuner.FILTER_TYPE_PCR);
             if (mEventDetector != null) {
                 mEventDetector.startDetecting(
+                        channel.getDeliverySystemType(),
                         channel.getFrequency(),
                         channel.getModulation(),
                         channel.getProgramNumber());
@@ -199,9 +222,11 @@
 
     @Override
     public boolean startStream(ScanChannel channel) {
-        if (mTunerHal.tune(channel.frequency, channel.modulation, null)) {
+        if (mTunerHal.tune(channel.deliverySystemType.getNumber(), channel.frequency,
+                channel.modulation, null)) {
             mEventDetector.startDetecting(
-                    channel.frequency, channel.modulation, EventDetector.ALL_PROGRAM_NUMBERS);
+                    channel.deliverySystemType, channel.frequency, channel.modulation,
+                    EventDetector.ALL_PROGRAM_NUMBERS);
             synchronized (mCircularBufferMonitor) {
                 if (mStreaming) {
                     Log.w(TAG, "Streaming should be stopped before start streaming");
@@ -295,7 +320,7 @@
     public void registerListener(EventListener listener) {
         if (mEventDetector != null && listener != null) {
             synchronized (mEventListenerActions) {
-                mEventListenerActions.add(new Pair<>(listener, true));
+                mEventListenerActions.add(Pair.create(listener, true));
             }
         }
     }
diff --git a/tuner/src/com/android/tv/tuner/ts/EventDetector.java b/tuner/src/com/android/tv/tuner/ts/EventDetector.java
index 6d1fc27..3a2d835 100644
--- a/tuner/src/com/android/tv/tuner/ts/EventDetector.java
+++ b/tuner/src/com/android/tv/tuner/ts/EventDetector.java
@@ -20,12 +20,13 @@
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 import com.android.tv.tuner.api.Tuner;
+import com.android.tv.tuner.data.Channel.DeliverySystemType;
 import com.android.tv.tuner.data.PsiData;
 import com.android.tv.tuner.data.PsipData;
 import com.android.tv.tuner.data.PsipData.EitItem;
+import com.android.tv.tuner.data.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
 import com.android.tv.tuner.data.TunerChannel;
-import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -51,6 +52,7 @@
     private final SparseBooleanArray mVctCaptionTracksFound = new SparseBooleanArray();
     private final SparseBooleanArray mEitCaptionTracksFound = new SparseBooleanArray();
     private final List<EventListener> mEventListeners = new ArrayList<>();
+    private DeliverySystemType mDeliverySystemType;
     private int mFrequency;
     private String mModulation;
     private int mProgramNumber = ALL_PROGRAM_NUMBERS;
@@ -170,6 +172,7 @@
                     }
                     tunerChannel.setAudioTracks(audioTracks);
                     tunerChannel.setCaptionTracks(captionTracks);
+                    tunerChannel.setDeliverySystemType(mDeliverySystemType);
                     tunerChannel.setFrequency(mFrequency);
                     tunerChannel.setModulation(mModulation);
                     mChannelMap.put(tunerChannel.getProgramNumber(), tunerChannel);
@@ -209,6 +212,7 @@
                     int channelProgramNumber = channel.getServiceId();
                     tunerChannel.setAudioTracks(audioTracks);
                     tunerChannel.setCaptionTracks(captionTracks);
+                    tunerChannel.setDeliverySystemType(mDeliverySystemType);
                     tunerChannel.setFrequency(mFrequency);
                     tunerChannel.setModulation(mModulation);
                     mChannelMap.put(tunerChannel.getProgramNumber(), tunerChannel);
@@ -252,10 +256,18 @@
 
     private void reset() {
         // TODO: Use TsParser.reset()
+        int[] deliverySystemTypes = mTunerHal.getDeliverySystemTypes();
+        boolean isDvbSignal = false;
+        for (int i = 0; i < deliverySystemTypes.length; i++) {
+            if (Tuner.isDvbDeliverySystem(deliverySystemTypes[i])) {
+                isDvbSignal = true;
+                break;
+            }
+        }
         mTsParser =
                 new TsParser(
                         mTsOutputListener,
-                        Tuner.isDvbDeliverySystem(mTunerHal.getDeliverySystemType()));
+                        isDvbSignal);
         mPidSet.clear();
         mVctProgramNumberSet.clear();
         mSdtProgramNumberSet.clear();
@@ -272,8 +284,10 @@
      * @param programNumber The program number if this is for handling tune request. For scanning
      *     purpose, supply {@link #ALL_PROGRAM_NUMBERS}.
      */
-    public void startDetecting(int frequency, String modulation, int programNumber) {
+    public void startDetecting(DeliverySystemType deliverySystemType, int frequency,
+                               String modulation, int programNumber) {
         reset();
+        mDeliverySystemType = deliverySystemType;
         mFrequency = frequency;
         mModulation = modulation;
         mProgramNumber = programNumber;
diff --git a/tuner/src/com/android/tv/tuner/tvinput/BaseTunerTvInputService.java b/tuner/src/com/android/tv/tuner/tvinput/BaseTunerTvInputService.java
index d22b639..2053b2a 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/BaseTunerTvInputService.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/BaseTunerTvInputService.java
@@ -21,17 +21,29 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.media.tv.TvInputService;
+import android.net.Uri;
 import android.util.Log;
+
 import com.android.tv.common.feature.CommonFeatures;
 import com.android.tv.tuner.source.TsDataSourceManager;
 import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
+import com.android.tv.tuner.tvinput.factory.TunerRecordingSessionFactory;
 import com.android.tv.tuner.tvinput.factory.TunerSessionFactory;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.cache.RemovalListener;
+
 import dagger.android.AndroidInjection;
+
 import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+
 import java.util.Collections;
 import java.util.Set;
 import java.util.WeakHashMap;
 import java.util.concurrent.TimeUnit;
+
 import javax.inject.Inject;
 
 /** {@link BaseTunerTvInputService} serves TV channels coming from a tuner device. */
@@ -42,10 +54,22 @@
     private static final int DVR_STORAGE_CLEANUP_JOB_ID = 100;
 
     private final Set<Session> mTunerSessions = Collections.newSetFromMap(new WeakHashMap<>());
+    private final Set<RecordingSession> mTunerRecordingSession =
+            Collections.newSetFromMap(new WeakHashMap<>());
     private ChannelDataManager mChannelDataManager;
     @Inject ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
     @Inject TsDataSourceManager.Factory mTsDataSourceManagerFactory;
     @Inject TunerSessionFactory mTunerSessionFactory;
+    @Inject TunerRecordingSessionFactory mTunerRecordingSessionFactory;
+
+    LoadingCache<String, ChannelDataManager> mChannelDataManagers;
+    RemovalListener<String, ChannelDataManager> mChannelDataManagerRemovalListener =
+            notification -> {
+                ChannelDataManager cdm = notification.getValue();
+                if (cdm != null) {
+                    cdm.release();
+                }
+            };
 
     @Override
     public void onCreate() {
@@ -57,7 +81,17 @@
         AndroidInjection.inject(this);
         super.onCreate();
         if (DEBUG) Log.d(TAG, "onCreate");
-        mChannelDataManager = new ChannelDataManager(getApplicationContext());
+        mChannelDataManagers =
+                CacheBuilder.newBuilder()
+                        .weakValues()
+                        .removalListener(mChannelDataManagerRemovalListener)
+                        .build(
+                                new CacheLoader<String, ChannelDataManager>() {
+                                    @Override
+                                    public ChannelDataManager load(String inputId) {
+                                        return createChannelDataManager(inputId);
+                                    }
+                                });
         if (CommonFeatures.DVR.isEnabled(this)) {
             JobScheduler jobScheduler =
                     (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
@@ -77,21 +111,24 @@
         }
     }
 
+    private ChannelDataManager createChannelDataManager(String inputId) {
+        return new ChannelDataManager(getApplicationContext(), inputId);
+    }
+
     @Override
     public void onDestroy() {
         if (DEBUG) Log.d(TAG, "onDestroy");
         super.onDestroy();
-        mChannelDataManager.release();
+        mChannelDataManagers.invalidateAll();
     }
 
     @Override
     public RecordingSession onCreateRecordingSession(String inputId) {
-        return new TunerRecordingSession(
-                this,
-                inputId,
-                mChannelDataManager,
-                mConcurrentDvrPlaybackFlags,
-                mTsDataSourceManagerFactory);
+        RecordingSession session =
+                mTunerRecordingSessionFactory.create(
+                        inputId, this::onReleased, mChannelDataManagers.getUnchecked(inputId));
+        mTunerRecordingSession.add(session);
+        return session;
     }
 
     @Override
@@ -103,8 +140,12 @@
                 Log.d(TAG, "abort creating an session");
                 return null;
             }
+
             final Session session =
-                    mTunerSessionFactory.create(this, mChannelDataManager, this::onReleased);
+                    mTunerSessionFactory.create(
+                            mChannelDataManagers.getUnchecked(inputId),
+                            this::onReleased,
+                            this::getRecordingUri);
             mTunerSessions.add(session);
             session.setOverlayViewEnabled(true);
             return session;
@@ -115,7 +156,22 @@
         }
     }
 
+    private Uri getRecordingUri(Uri channelUri) {
+        for (RecordingSession session : mTunerRecordingSession) {
+            TunerRecordingSession tunerSession = (TunerRecordingSession) session;
+            if (tunerSession.getChannelUri().equals(channelUri)) {
+                return tunerSession.getRecordingUri();
+            }
+        }
+        return null;
+    }
+
     private void onReleased(Session session) {
         mTunerSessions.remove(session);
+        mChannelDataManagers.cleanUp();
+    }
+
+    private void onReleased(RecordingSession session) {
+        mTunerRecordingSession.remove(session);
     }
 }
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java b/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java
index 5561693..ed61f71 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java
@@ -22,33 +22,40 @@
 import android.support.annotation.Nullable;
 import android.support.annotation.WorkerThread;
 import android.util.Log;
+
 import com.android.tv.common.compat.RecordingSessionCompat;
-import com.android.tv.tuner.source.TsDataSourceManager;
+import com.android.tv.common.dagger.annotations.ApplicationContext;
 import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
-import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+import com.android.tv.tuner.tvinput.factory.TunerRecordingSessionFactory;
+import com.android.tv.tuner.tvinput.factory.TunerRecordingSessionFactory.RecordingSessionReleasedCallback;
+
+import com.google.auto.factory.AutoFactory;
+import com.google.auto.factory.Provided;
 
 /** Processes DVR recordings, and deletes the previously recorded contents. */
+@AutoFactory(
+        className = "TunerRecordingSessionFactoryImpl",
+        implementing = TunerRecordingSessionFactory.class)
 public class TunerRecordingSession extends RecordingSessionCompat {
     private static final String TAG = "TunerRecordingSession";
     private static final boolean DEBUG = false;
 
     private final TunerRecordingSessionWorker mSessionWorker;
+    private final RecordingSessionReleasedCallback mReleasedCallback;
+    private Uri mChannelUri;
+    private Uri mRecordingUri;
 
     public TunerRecordingSession(
-            Context context,
+            @Provided @ApplicationContext Context context,
             String inputId,
+            RecordingSessionReleasedCallback releasedCallback,
             ChannelDataManager channelDataManager,
-            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
-            TsDataSourceManager.Factory tsDataSourceManagerFactory) {
+            @Provided TunerRecordingSessionWorker.Factory tunerRecordingSessionWorkerFactory) {
         super(context);
+        mReleasedCallback = releasedCallback;
         mSessionWorker =
-                new TunerRecordingSessionWorker(
-                        context,
-                        inputId,
-                        channelDataManager,
-                        this,
-                        concurrentDvrPlaybackFlags,
-                        tsDataSourceManagerFactory);
+                tunerRecordingSessionWorkerFactory.create(
+                        context, inputId, channelDataManager, this);
     }
 
     // RecordingSession
@@ -69,6 +76,7 @@
             Log.d(TAG, "Requesting recording session release.");
         }
         mSessionWorker.release();
+        mReleasedCallback.onReleased(this);
     }
 
     @MainThread
@@ -95,6 +103,7 @@
         if (DEBUG) {
             Log.d(TAG, "Notifying recording session tuned.");
         }
+        mChannelUri = channelUri;
         notifyTuned(channelUri);
     }
 
@@ -112,6 +121,7 @@
         if (DEBUG) {
             Log.d(TAG, "Notifying record successfully finished.");
         }
+        mRecordingUri = null;
         notifyRecordingStopped(recordedProgramUri);
     }
 
@@ -120,4 +130,19 @@
         Log.w(TAG, "Notifying recording error: " + reason);
         notifyError(reason);
     }
+
+    public void onRecordingStatePartial(Uri recUri) {
+        if (DEBUG) {
+            Log.d(TAG, "Updating recording session state to Partial");
+        }
+        mRecordingUri = recUri;
+    }
+
+    public Uri getChannelUri() {
+        return mChannelUri;
+    }
+
+    public Uri getRecordingUri() {
+        return mRecordingUri;
+    }
 }
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java b/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java
index 2c0c09a..851593a 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java
@@ -37,17 +37,19 @@
 import android.support.annotation.Nullable;
 import android.util.Log;
 import android.util.Pair;
+
 import androidx.tvprovider.media.tv.Program;
+
 import com.android.tv.common.BaseApplication;
 import com.android.tv.common.data.RecordedProgramState;
 import com.android.tv.common.recording.RecordingCapability;
 import com.android.tv.common.recording.RecordingStorageStatusManager;
 import com.android.tv.common.util.CommonUtils;
-import com.android.tv.tuner.DvbDeviceAccessor;
 import com.android.tv.tuner.data.PsipData;
 import com.android.tv.tuner.data.PsipData.EitItem;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
 import com.android.tv.tuner.data.TunerChannel;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.dvb.DvbDeviceAccessor;
 import com.android.tv.tuner.exoplayer.ExoPlayerSampleExtractor;
 import com.android.tv.tuner.exoplayer.SampleExtractor;
 import com.android.tv.tuner.exoplayer.buffer.BufferManager;
@@ -57,8 +59,11 @@
 import com.android.tv.tuner.source.TsDataSourceManager;
 import com.android.tv.tuner.ts.EventDetector.EventListener;
 import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
+
 import com.google.android.exoplayer.C;
-import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+import com.google.auto.factory.AutoFactory;
+import com.google.auto.factory.Provided;
+
 import java.io.File;
 import java.io.IOException;
 import java.lang.annotation.Retention;
@@ -142,7 +147,6 @@
 
     private static final long CHANNEL_ID_NONE = -1;
     private static final int MAX_TUNING_RETRY = 6;
-    private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
 
     private final Context mContext;
     private final ChannelDataManager mChannelDataManager;
@@ -169,14 +173,27 @@
     private List<AtscCaptionTrack> mCaptionTracks;
     private DvrStorageManager mDvrStorageManager;
 
+    /**
+     * Factory for {@link TunerRecordingSessionWorker}}.
+     *
+     * <p>This wrapper class keeps other classes from needing to reference the {@link AutoFactory}
+     * generated class.
+     */
+    public interface Factory {
+        TunerRecordingSessionWorker create(
+                Context context,
+                String inputId,
+                ChannelDataManager dataManager,
+                TunerRecordingSession session);
+    }
+
+    @AutoFactory(implementing = TunerRecordingSessionWorker.Factory.class)
     public TunerRecordingSessionWorker(
             Context context,
             String inputId,
             ChannelDataManager dataManager,
             TunerRecordingSession session,
-            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
-            TsDataSourceManager.Factory tsDataSourceManagerFactory) {
-        mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
+            @Provided TsDataSourceManager.Factory tsDataSourceManagerFactory) {
         mRandom.setSeed(System.nanoTime());
         mContext = context;
         HandlerThread handlerThread = new HandlerThread(TAG);
@@ -217,7 +234,7 @@
         if (mChannel == null || mChannel.compareTo(channel) != 0) {
             return;
         }
-        mHandler.obtainMessage(MSG_UPDATE_CC_INFO, new Pair<>(channel, items)).sendToTarget();
+        mHandler.obtainMessage(MSG_UPDATE_CC_INFO, Pair.create(channel, items)).sendToTarget();
         mChannelDataManager.notifyEventDetected(channel, items);
     }
 
@@ -362,7 +379,7 @@
                 }
             case MSG_UPDATE_PARTIAL_STATE:
                 {
-                    updateRecordedProgram(RecordedProgramState.PARTIAL, -1, -1);
+                    updateRecordedProgramStatePartial();
                     return true;
                 }
         }
@@ -457,35 +474,29 @@
         mDvrStorageManager = new DvrStorageManager(mStorageDir, true);
         mRecorder =
                 new ExoPlayerSampleExtractor(
-                        Uri.EMPTY,
-                        mTunerSource,
-                        new BufferManager(mDvrStorageManager),
-                        this,
-                        true,
-                        mConcurrentDvrPlaybackFlags);
+                        Uri.EMPTY, mTunerSource, new BufferManager(mDvrStorageManager), this, true);
         mRecorder.setOnCompletionListener(this, mHandler);
         mProgramUri = programUri;
         mSessionState = STATE_RECORDING;
         mRecorderRunning = true;
-        if (mConcurrentDvrPlaybackFlags.enabled()) {
-            mRecordedProgramUri =
-                    insertRecordedProgram(
-                            getRecordedProgram(),
-                            mChannel.getChannelId(),
-                            Uri.fromFile(mStorageDir).toString(),
-                            calculateRecordingSizeInBytes(),
-                            mRecordStartTime,
-                            mRecordStartTime);
-            if (mRecordedProgramUri == null) {
-                new DeleteRecordingTask().execute(mStorageDir);
-                mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
-                Log.e(TAG, "Inserting a recording to DB failed");
-                return false;
-            }
-            mSession.onRecordingUri(mRecordedProgramUri.toString());
-            mHandler.sendEmptyMessageDelayed(
-                    MSG_UPDATE_PARTIAL_STATE, MIN_PARTIAL_RECORDING_DURATION_MS);
+        mRecordedProgramUri =
+                insertRecordedProgram(
+                        getRecordedProgram(),
+                        mChannel.getChannelId(),
+                        Uri.fromFile(mStorageDir).toString(),
+                        calculateRecordingSizeInBytes(),
+                        mRecordStartTime,
+                        mRecordStartTime);
+        if (mRecordedProgramUri == null) {
+            new DeleteRecordingTask().execute(mStorageDir);
+            mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
+            Log.e(TAG, "Inserting a recording to DB failed");
+            return false;
         }
+        mSession.onRecordingUri(mRecordedProgramUri.toString());
+        mHandler.sendEmptyMessageDelayed(
+                MSG_UPDATE_PARTIAL_STATE, MIN_PARTIAL_RECORDING_DURATION_MS);
+
         mHandler.sendEmptyMessage(MSG_PREPARE_RECODER);
         mHandler.removeMessages(MSG_MONITOR_STORAGE_STATUS);
         mHandler.sendEmptyMessageDelayed(MSG_MONITOR_STORAGE_STATUS, STORAGE_MONITOR_INTERVAL_MS);
@@ -592,7 +603,7 @@
         if (checkRecordedProgramTable(COLUMN_SERIES_ID)) {
             values.put(COLUMN_SERIES_ID, mSeriesId);
         }
-        if (mConcurrentDvrPlaybackFlags.enabled() && checkRecordedProgramTable(COLUMN_STATE)) {
+        if (checkRecordedProgramTable(COLUMN_STATE)) {
             values.put(COLUMN_STATE, RecordedProgramState.STARTED.name());
         }
         if (program != null) {
@@ -602,20 +613,27 @@
                 .insert(TvContract.RecordedPrograms.CONTENT_URI, values);
     }
 
-    private void updateRecordedProgram(RecordedProgramState state, long endTime, long totalBytes) {
+    private void updateRecordedProgramStateFinished(long endTime, long totalBytes) {
         ContentValues values = new ContentValues();
+        values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, totalBytes);
+        values.put(
+                RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, endTime - mRecordStartTime);
+        values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, endTime);
         if (checkRecordedProgramTable(COLUMN_STATE)) {
-            values.put(COLUMN_STATE, state.name());
-        }
-        if (state.equals(RecordedProgramState.FINISHED)) {
-            values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, totalBytes);
-            values.put(
-                    RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, endTime - mRecordStartTime);
-            values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, endTime);
+            values.put(COLUMN_STATE, RecordedProgramState.FINISHED.name());
         }
         mContext.getContentResolver().update(mRecordedProgramUri, values, null, null);
     }
 
+    private void updateRecordedProgramStatePartial() {
+        mSession.onRecordingStatePartial(mRecordedProgramUri);
+        if (checkRecordedProgramTable(COLUMN_STATE)) {
+            ContentValues values = new ContentValues();
+            values.put(COLUMN_STATE, RecordedProgramState.PARTIAL.name());
+            mContext.getContentResolver().update(mRecordedProgramUri, values, null, null);
+        }
+    }
+
     private void onRecordingResult(boolean success, long lastExtractedPositionUs) {
         if (mSessionState != STATE_RECORDING) {
             // Error notification is not needed.
@@ -640,25 +658,7 @@
                 (lastExtractedPositionUs == C.UNKNOWN_TIME_US)
                         ? System.currentTimeMillis()
                         : mRecordStartTime + lastExtractedPositionUs / 1000;
-        if (!mConcurrentDvrPlaybackFlags.enabled()) {
-            mRecordedProgramUri =
-                    insertRecordedProgram(
-                            getRecordedProgram(),
-                            mChannel.getChannelId(),
-                            Uri.fromFile(mStorageDir).toString(),
-                            calculateRecordingSizeInBytes(),
-                            mRecordStartTime,
-                            recordEndTime);
-            if (mRecordedProgramUri == null) {
-                new DeleteRecordingTask().execute(mStorageDir);
-                mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
-                Log.e(TAG, "Inserting a recording to DB failed");
-                return;
-            }
-        } else {
-            updateRecordedProgram(
-                    RecordedProgramState.FINISHED, recordEndTime, calculateRecordingSizeInBytes());
-        }
+        updateRecordedProgramStateFinished(recordEndTime, calculateRecordingSizeInBytes());
         mDvrStorageManager.writeCaptionInfoFiles(mCaptionTracks);
         mSession.onRecordFinished(mRecordedProgramUri);
     }
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerSession.java b/tuner/src/com/android/tv/tuner/tvinput/TunerSession.java
index fedb5f6..eb3a7d0 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/TunerSession.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/TunerSession.java
@@ -27,18 +27,24 @@
 import android.util.Log;
 import android.view.Surface;
 import android.view.View;
+
 import com.android.tv.common.CommonPreferences.CommonPreferencesChangedListener;
 import com.android.tv.common.compat.TisSessionCompat;
+import com.android.tv.common.dagger.annotations.ApplicationContext;
 import com.android.tv.tuner.prefs.TunerPreferences;
-import com.android.tv.tuner.source.TsDataSourceManager;
 import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactory;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactory.SessionRecordingCallback;
 import com.android.tv.tuner.tvinput.factory.TunerSessionFactory.SessionReleasedCallback;
-import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+
+import com.google.auto.factory.AutoFactory;
+import com.google.auto.factory.Provided;
 
 /**
  * Provides a tuner TV input session. Main tuner input functions are implemented in {@link
  * TunerSessionWorker}.
  */
+@AutoFactory(className = "TunerSessionV1Factory", implementing = TunerSessionFactory.class)
 public class TunerSession extends TisSessionCompat implements CommonPreferencesChangedListener {
 
     private static final String TAG = "TunerSession";
@@ -47,26 +53,26 @@
     private final TunerSessionOverlay mTunerSessionOverlay;
     private final TunerSessionWorker mSessionWorker;
     private final SessionReleasedCallback mReleasedCallback;
+    private final SessionRecordingCallback mRecordingCallback;
     private boolean mPlayPaused;
     private long mTuneStartTimestamp;
 
     public TunerSession(
-            Context context,
+            @Provided @ApplicationContext Context context,
             ChannelDataManager channelDataManager,
             SessionReleasedCallback releasedCallback,
-            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
-            TsDataSourceManager.Factory tsDataSourceManagerFactory) {
+            SessionRecordingCallback recordingCallback,
+            @Provided TunerSessionWorker.Factory tunerSessionWorkerFactory) {
         super(context);
         mReleasedCallback = releasedCallback;
+        mRecordingCallback = recordingCallback;
         mTunerSessionOverlay = new TunerSessionOverlay(context);
         mSessionWorker =
-                new TunerSessionWorker(
+                tunerSessionWorkerFactory.create(
                         context,
                         channelDataManager,
                         this,
-                        mTunerSessionOverlay,
-                        concurrentDvrPlaybackFlags,
-                        tsDataSourceManagerFactory);
+                        mTunerSessionOverlay);
         TunerPreferences.setCommonPreferencesChangedListener(this);
     }
 
@@ -204,4 +210,8 @@
     public void onCommonPreferencesChanged() {
         mSessionWorker.sendMessage(TunerSessionWorker.MSG_TUNER_PREFERENCES_CHANGED);
     }
+
+    public Uri getRecordingUri(Uri channelUri) {
+        return mRecordingCallback.getRecordingUri(channelUri);
+    }
 }
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerSessionExoV2.java b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionExoV2.java
index 4eca44d..7ebb2b2 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/TunerSessionExoV2.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionExoV2.java
@@ -27,15 +27,21 @@
 import android.util.Log;
 import android.view.Surface;
 import android.view.View;
+
 import com.android.tv.common.CommonPreferences.CommonPreferencesChangedListener;
 import com.android.tv.common.compat.TisSessionCompat;
+import com.android.tv.common.dagger.annotations.ApplicationContext;
 import com.android.tv.tuner.prefs.TunerPreferences;
-import com.android.tv.tuner.source.TsDataSourceManager;
 import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactory;
+import com.android.tv.tuner.tvinput.factory.TunerSessionFactory.SessionRecordingCallback;
 import com.android.tv.tuner.tvinput.factory.TunerSessionFactory.SessionReleasedCallback;
-import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+
+import com.google.auto.factory.AutoFactory;
+import com.google.auto.factory.Provided;
 
 /** Provides a tuner TV input session. */
+@AutoFactory(implementing = TunerSessionFactory.class)
 public class TunerSessionExoV2 extends TisSessionCompat
         implements CommonPreferencesChangedListener {
 
@@ -45,26 +51,26 @@
     private final TunerSessionOverlay mTunerSessionOverlay;
     private final TunerSessionWorkerExoV2 mSessionWorker;
     private final SessionReleasedCallback mReleasedCallback;
+    private final SessionRecordingCallback mRecordingCallback;
     private boolean mPlayPaused;
     private long mTuneStartTimestamp;
 
     public TunerSessionExoV2(
-            Context context,
+            @Provided @ApplicationContext Context context,
             ChannelDataManager channelDataManager,
             SessionReleasedCallback releasedCallback,
-            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
-            TsDataSourceManager.Factory tsDataSourceManagerFactory) {
+            SessionRecordingCallback recordingCallback,
+            @Provided TunerSessionWorkerExoV2.Factory tunerSessionWorkerExoV2Factory) {
         super(context);
         mReleasedCallback = releasedCallback;
+        mRecordingCallback = recordingCallback;
         mTunerSessionOverlay = new TunerSessionOverlay(context);
         mSessionWorker =
-                new TunerSessionWorkerExoV2(
+                tunerSessionWorkerExoV2Factory.create(
                         context,
                         channelDataManager,
                         this,
-                        mTunerSessionOverlay,
-                        concurrentDvrPlaybackFlags,
-                        tsDataSourceManagerFactory);
+                        mTunerSessionOverlay);
         TunerPreferences.setCommonPreferencesChangedListener(this);
     }
 
@@ -203,4 +209,8 @@
     public void onCommonPreferencesChanged() {
         mSessionWorker.sendMessage(TunerSessionWorkerExoV2.MSG_TUNER_PREFERENCES_CHANGED);
     }
+
+    public Uri getRecordingUri(Uri channelUri) {
+        return mRecordingCallback.getRecordingUri(channelUri);
+    }
 }
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerSessionOverlay.java b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionOverlay.java
index 9f21e16..53e0bcc 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/TunerSessionOverlay.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionOverlay.java
@@ -26,17 +26,18 @@
 import android.view.ViewGroup;
 import android.widget.TextView;
 import android.widget.Toast;
-import com.android.tv.common.util.SystemPropertiesProxy;
+
 import com.android.tv.tuner.R;
 import com.android.tv.tuner.cc.CaptionLayout;
 import com.android.tv.tuner.cc.CaptionTrackRenderer;
 import com.android.tv.tuner.data.Cea708Data.CaptionEvent;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
 import com.android.tv.tuner.util.GlobalSettingsUtils;
 import com.android.tv.tuner.util.StatusTextUtils;
 
 /** Executes {@link Session} overlay changes on the main thread. */
 /* package */ final class TunerSessionOverlay implements Handler.Callback {
+    private static final boolean DEBUG = false;
 
     /** Displays the given {@link String} message object in the message view. */
     public static final int MSG_UI_SHOW_MESSAGE = 1;
@@ -67,8 +68,6 @@
     /** Displays a toast signalling that a re-scan is required. Does not expect a message object. */
     public static final int MSG_UI_TOAST_RESCAN_NEEDED = 11;
 
-    private static final String USBTUNER_SHOW_DEBUG = "persist.tv.tuner.show_debug";
-
     private final Context mContext;
     private final Handler mHandler;
     private final View mOverlayView;
@@ -88,13 +87,12 @@
         mHandler = new Handler(this);
         LayoutInflater inflater =
                 (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-        boolean showDebug = SystemPropertiesProxy.getBoolean(USBTUNER_SHOW_DEBUG, false);
         mOverlayView = inflater.inflate(R.layout.ut_overlay_view, null);
         mMessageLayout = mOverlayView.findViewById(R.id.message_layout);
         mMessageLayout.setVisibility(View.INVISIBLE);
         mMessageView = mOverlayView.findViewById(R.id.message);
         mStatusView = mOverlayView.findViewById(R.id.tuner_status);
-        mStatusView.setVisibility(showDebug ? View.VISIBLE : View.INVISIBLE);
+        mStatusView.setVisibility(DEBUG ? View.VISIBLE : View.INVISIBLE);
         mAudioStatusView = mOverlayView.findViewById(R.id.audio_status);
         mAudioStatusView.setVisibility(View.INVISIBLE);
         CaptionLayout captionLayout = mOverlayView.findViewById(R.id.caption);
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java
index d3f9409..f9bc734 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java
@@ -44,22 +44,22 @@
 import android.util.SparseArray;
 import android.view.Surface;
 import android.view.accessibility.CaptioningManager;
+
 import com.android.tv.common.CommonPreferences.TrickplaySetting;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.TvContentRatingCache;
 import com.android.tv.common.compat.TvInputConstantCompat;
 import com.android.tv.common.customization.CustomizationManager;
 import com.android.tv.common.customization.CustomizationManager.TRICKPLAY_MODE;
-import com.android.tv.common.experiments.Experiments;
+import com.android.tv.common.dev.DeveloperPreferences;
 import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.common.util.SystemPropertiesProxy;
 import com.android.tv.tuner.data.Cea708Data;
+import com.android.tv.tuner.data.Channel;
 import com.android.tv.tuner.data.PsipData.EitItem;
 import com.android.tv.tuner.data.PsipData.TvTracksInterface;
+import com.android.tv.tuner.data.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
 import com.android.tv.tuner.data.TunerChannel;
-import com.android.tv.tuner.data.nano.Channel;
-import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
 import com.android.tv.tuner.exoplayer.MpegTsPlayer;
 import com.android.tv.tuner.exoplayer.MpegTsRendererBuilder;
 import com.android.tv.tuner.exoplayer.buffer.BufferManager;
@@ -74,10 +74,16 @@
 import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
 import com.android.tv.tuner.tvinput.debug.TunerDebug;
 import com.android.tv.tuner.util.StatusTextUtils;
+
 import com.google.android.exoplayer.ExoPlayer;
 import com.google.android.exoplayer.audio.AudioCapabilities;
+import com.google.auto.factory.AutoFactory;
+import com.google.auto.factory.Provided;
 import com.google.common.collect.ImmutableList;
+
 import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+import com.android.tv.common.flags.LegacyFlags;
+
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Iterator;
@@ -103,8 +109,6 @@
     private static final boolean DEBUG = false;
     private static final boolean ENABLE_PROFILER = true;
     private static final String PLAY_FROM_CHANNEL = "channel";
-    private static final String MAX_BUFFER_SIZE_KEY = "tv.tuner.buffersize_mbytes";
-    private static final int MAX_BUFFER_SIZE_DEF = 2 * 1024; // 2GB
     private static final int MIN_BUFFER_SIZE_DEF = 256; // 256MB
 
     // Public messages
@@ -189,6 +193,7 @@
     private final int mMaxTrickplayBufferSizeMb;
     private final File mTrickplayBufferDir;
     private final @TRICKPLAY_MODE int mTrickplayModeCustomization;
+    private final LegacyFlags mLegacyFlags;
     private volatile Surface mSurface;
     private volatile float mVolume = 1.0f;
     private volatile boolean mCaptionEnabled;
@@ -232,17 +237,36 @@
     private boolean mReleaseRequested; // Guarded by mReleaseLock
     private final Object mReleaseLock = new Object();
     private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
+    private Uri mChannelUri;
+    private Uri mRecordingUri;
+    private boolean mOnTuneUsesRecording = false;
 
     private int mSignalStrength;
     private long mRecordedProgramStartTimeMs;
 
+    /**
+     * Factory for {@link TunerSessionWorker}.
+     *
+     * <p>This wrapper class keeps other classes from needing to reference the {@link AutoFactory}
+     * generated class.
+     */
+    public interface Factory {
+        public TunerSessionWorker create(
+                Context context,
+                ChannelDataManager channelDataManager,
+                TunerSession tunerSession,
+                TunerSessionOverlay tunerSessionOverlay);
+    }
+
+    @AutoFactory(implementing = TunerSessionWorker.Factory.class)
     public TunerSessionWorker(
             Context context,
             ChannelDataManager channelDataManager,
             TunerSession tunerSession,
             TunerSessionOverlay tunerSessionOverlay,
-            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
-            TsDataSourceManager.Factory tsDataSourceManagerFactory) {
+            @Provided ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
+            @Provided LegacyFlags legacyFlags,
+            @Provided TsDataSourceManager.Factory tsDataSourceManagerFactory) {
         this(
                 context,
                 channelDataManager,
@@ -250,6 +274,7 @@
                 tunerSessionOverlay,
                 null,
                 concurrentDvrPlaybackFlags,
+                legacyFlags,
                 tsDataSourceManagerFactory);
     }
 
@@ -261,8 +286,10 @@
             TunerSessionOverlay tunerSessionOverlay,
             @Nullable Handler handler,
             ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
+            LegacyFlags legacyFlags,
             TsDataSourceManager.Factory tsDataSourceManagerFactory) {
         this.mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
+        mLegacyFlags = legacyFlags;
         if (DEBUG) Log.d(TAG, "TunerSessionWorker created");
         mContext = context;
         if (handler != null) {
@@ -277,6 +304,7 @@
         mSession = tunerSession;
         mTunerSessionOverlay = tunerSessionOverlay;
         mChannelDataManager = channelDataManager;
+        mRecordingUri = null;
         mChannelDataManager.setListener(this);
         mChannelDataManager.checkDataVersion(mContext);
         mSourceManager = tsDataSourceManagerFactory.create(false);
@@ -293,8 +321,7 @@
                 (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
         mCaptionEnabled = captioningManager.isEnabled();
         mPlaybackParams.setSpeed(1.0f);
-        mMaxTrickplayBufferSizeMb =
-                SystemPropertiesProxy.getInt(MAX_BUFFER_SIZE_KEY, MAX_BUFFER_SIZE_DEF);
+        mMaxTrickplayBufferSizeMb = DeveloperPreferences.MAX_BUFFER_SIZE_MBYTES.get(context);
         mTrickplayModeCustomization = CustomizationManager.getTrickplayMode(context);
         if (mTrickplayModeCustomization
                 == CustomizationManager.TRICKPLAY_MODE_USE_EXTERNAL_STORAGE) {
@@ -487,6 +514,11 @@
             // Final status
             // notification of STATE_ENDED from MpegTsPlayer will be ignored afterwards.
             Log.i(TAG, "Player ended: end of stream");
+            if (mOnTuneUsesRecording) {
+                mRecordingUri = null;
+                mSession.notifyChannelRetuned(mChannelUri);
+                sendMessage(MSG_TUNE, mChannelUri);
+            }
             if (mChannel != null) {
                 sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer));
             }
@@ -515,10 +547,10 @@
     @Override
     public void onVideoSizeChanged(int width, int height, float pixelWidthHeight) {
         if (mChannel != null && mChannel.hasVideo()) {
-            updateVideoTrack(width, height);
+            updateVideoTrack(width, height, pixelWidthHeight);
         }
         if (mRecordingId != null) {
-            updateVideoTrack(width, height);
+            updateVideoTrack(width, height, pixelWidthHeight);
         }
     }
 
@@ -532,6 +564,9 @@
             } else {
                 mBufferStartTimeMs = mRecordStartTimeMs = System.currentTimeMillis();
             }
+            if (mOnTuneUsesRecording) {
+                mBufferStartTimeMs = mRecordStartTimeMs = mRecordedProgramStartTimeMs;
+            }
             notifyVideoAvailable();
             mReportedDrawnToSurface = true;
 
@@ -587,7 +622,7 @@
     // ChannelDataManager.ProgramInfoListener
     @Override
     public void onProgramsArrived(TunerChannel channel, List<EitItem> programs) {
-        sendMessage(MSG_SCHEDULE_OF_PROGRAMS, new Pair<>(channel, programs));
+        sendMessage(MSG_SCHEDULE_OF_PROGRAMS, Pair.create(channel, programs));
     }
 
     @Override
@@ -602,7 +637,7 @@
 
     @Override
     public void onRequestProgramsResponse(TunerChannel channel, List<EitItem> programs) {
-        sendMessage(MSG_PROGRAM_DATA_RESULT, new Pair<>(channel, programs));
+        sendMessage(MSG_PROGRAM_DATA_RESULT, Pair.create(channel, programs));
     }
 
     // PlaybackBufferListener
@@ -650,7 +685,7 @@
     }
 
     private static class RecordedProgram {
-        //        private final long mChannelId;
+        private final long mChannelId;
         private final String mDataUri;
         private final long mStartTimeMillis;
 
@@ -662,14 +697,13 @@
 
         public RecordedProgram(Cursor cursor) {
             int index = 0;
-            //            mChannelId = cursor.getLong(index++);
-            index++;
+            mChannelId = cursor.getLong(index++);
             mDataUri = cursor.getString(index++);
             mStartTimeMillis = cursor.getLong(index++);
         }
 
         public RecordedProgram(long channelId, String dataUri) {
-            //            mChannelId = channelId;
+            mChannelId = channelId;
             mDataUri = dataUri;
             mStartTimeMillis = 0;
         }
@@ -689,6 +723,10 @@
         public long getStartTime() {
             return mStartTimeMillis;
         }
+
+        public long getChannelId() {
+            return mChannelId;
+        }
     }
 
     private RecordedProgram getRecordedProgram(Uri recordedUri) {
@@ -711,9 +749,13 @@
         }
     }
 
-    private String parseRecording(Uri uri) {
+    private String parseRecording(Uri uri, long channelId) {
         RecordedProgram recording = getRecordedProgram(uri);
         if (recording != null) {
+            if (channelId != -1 && channelId != recording.getChannelId()) {
+                // Recorded URI is of some other channel
+                return null;
+            }
             mRecordedProgramStartTimeMs = recording.getStartTime();
             return recording.getDataUri();
         }
@@ -826,10 +868,20 @@
             mIsActiveSession = true;
         }
         String recording = null;
+        mOnTuneUsesRecording = false;
         long channelId = parseChannel(channelUri);
         TunerChannel channel = (channelId == -1) ? null : mChannelDataManager.getChannel(channelId);
+        mRecordingUri = mSession.getRecordingUri(channelUri);
         if (channelId == -1) {
-            recording = parseRecording(channelUri);
+            recording = parseRecording(channelUri, channelId);
+
+        } else if (mRecordingUri != null && mConcurrentDvrPlaybackFlags.onTuneUsesRecording()) {
+            mChannelUri = channelUri;
+            recording = parseRecording(mRecordingUri, channelId);
+            if (recording != null) {
+                mOnTuneUsesRecording = true;
+                channel = null;
+            }
         }
         if (channel == null && recording == null) {
             Log.w(TAG, "onTune() is failed. Can't find channel for " + channelUri);
@@ -1133,8 +1185,15 @@
         if (mPlayer == null) {
             return true;
         }
+        long seekPosMs = timeMs;
+        if (mRecordingId != null) {
+            long systemBufferTime = System.currentTimeMillis() - SEEK_MARGIN_MS;
+            if (seekPosMs > systemBufferTime) {
+                seekPosMs = systemBufferTime;
+            }
+        }
         setTrickplayEnabledIfNeeded();
-        doTimeShiftSeekTo(timeMs);
+        doTimeShiftSeekTo(seekPosMs);
         return true;
     }
 
@@ -1432,8 +1491,7 @@
         }
         MpegTsPlayer player =
                 new MpegTsPlayer(
-                        new MpegTsRendererBuilder(
-                                mContext, bufferManager, this, mConcurrentDvrPlaybackFlags),
+                        new MpegTsRendererBuilder(mContext, bufferManager, this),
                         mHandler,
                         mSourceManager,
                         capabilities,
@@ -1444,7 +1502,7 @@
         player.setVideoEventListener(this);
         player.setCaptionServiceNumber(
                 mCaptionTrack != null
-                        ? mCaptionTrack.serviceNumber
+                        ? mCaptionTrack.getServiceNumber()
                         : Cea708Data.EMPTY_SERVICE_NUMBER);
         return player;
     }
@@ -1454,7 +1512,7 @@
             mTunerSessionOverlay.sendUiMessage(
                     TunerSessionOverlay.MSG_UI_START_CAPTION_TRACK, mCaptionTrack);
             if (mPlayer != null) {
-                mPlayer.setCaptionServiceNumber(mCaptionTrack.serviceNumber);
+                mPlayer.setCaptionServiceNumber(mCaptionTrack.getServiceNumber());
             }
         }
     }
@@ -1511,12 +1569,13 @@
         }
     }
 
-    private void updateVideoTrack(int width, int height) {
+    private void updateVideoTrack(int width, int height, float pixelWidthHeight) {
         removeTvTracks(TvTrackInfo.TYPE_VIDEO);
         mTvTracks.add(
                 new TvTrackInfo.Builder(TvTrackInfo.TYPE_VIDEO, VIDEO_TRACK_ID)
                         .setVideoWidth(width)
                         .setVideoHeight(height)
+                        .setVideoPixelAspectRatio(pixelWidthHeight)
                         .build());
         mSession.notifyTracksChanged(mTvTracks);
         mSession.notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, VIDEO_TRACK_ID);
@@ -1530,7 +1589,7 @@
         if (audioTracks != null) {
             int index = 0;
             for (AtscAudioTrack audioTrack : audioTracks) {
-                audioTrack.index = index;
+                audioTrack = audioTrack.toBuilder().setIndex(index).build();
                 mAudioTrackMap.put(index, audioTrack);
                 ++index;
             }
@@ -1560,10 +1619,10 @@
             String language =
                     !TextUtils.isEmpty(infoFromPlayer.language)
                             ? infoFromPlayer.language
-                            : (infoFromEit != null && infoFromEit.language != null)
-                                    ? infoFromEit.language
-                                    : (infoFromVct != null && infoFromVct.language != null)
-                                            ? infoFromVct.language
+                            : (infoFromEit != null && infoFromEit.hasLanguage())
+                                    ? infoFromEit.getLanguage()
+                                    : (infoFromVct != null && infoFromVct.hasLanguage())
+                                            ? infoFromVct.getLanguage()
                                             : null;
             TvTrackInfo.Builder builder =
                     new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, AUDIO_TRACK_PREFIX + i);
@@ -1584,20 +1643,20 @@
         mCaptionTrackMap.clear();
         if (captionTracks != null) {
             for (AtscCaptionTrack captionTrack : captionTracks) {
-                if (mCaptionTrackMap.indexOfKey(captionTrack.serviceNumber) >= 0) {
+                if (mCaptionTrackMap.indexOfKey(captionTrack.getServiceNumber()) >= 0) {
                     continue;
                 }
-                String language = captionTrack.language;
+                String language = captionTrack.getLanguage();
 
                 // The service number of the caption service is used for track id of a subtitle.
                 // Later, when a subtitle is chosen, track id will be passed on to TsParser.
                 TvTrackInfo.Builder builder =
                         new TvTrackInfo.Builder(
                                 TvTrackInfo.TYPE_SUBTITLE,
-                                SUBTITLE_TRACK_PREFIX + captionTrack.serviceNumber);
+                                SUBTITLE_TRACK_PREFIX + captionTrack.getServiceNumber());
                 builder.setLanguage(language);
                 mTvTracks.add(builder.build());
-                mCaptionTrackMap.put(captionTrack.serviceNumber, captionTrack);
+                mCaptionTrackMap.put(captionTrack.getServiceNumber(), captionTrack);
             }
         }
         mSession.notifyTracksChanged(mTvTracks);
@@ -1777,6 +1836,9 @@
         } else {
             mBufferStartTimeMs = mRecordStartTimeMs = System.currentTimeMillis();
         }
+        if (mOnTuneUsesRecording) {
+            mBufferStartTimeMs = mRecordStartTimeMs = mRecordedProgramStartTimeMs;
+        }
         mLastPositionMs = 0;
         mCaptionTrack = null;
         mSignalStrength = TvInputConstantCompat.SIGNAL_STRENGTH_UNKNOWN;
@@ -1784,6 +1846,14 @@
             mSession.notifySignalStrength(mSignalStrength);
         }
         mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
+        if (mOnTuneUsesRecording) {
+            mHandler.obtainMessage(
+                            MSG_TIMESHIFT_SEEK_TO,
+                            1,
+                            0,
+                            System.currentTimeMillis() - SEEK_MARGIN_MS)
+                    .sendToTarget();
+        }
     }
 
     private void doReschedulePrograms() {
@@ -1805,7 +1875,7 @@
                                 + " current program: "
                                 + getCurrentProgram());
             }
-            mHandler.obtainMessage(MSG_SCHEDULE_OF_PROGRAMS, new Pair<>(mChannel, mPrograms))
+            mHandler.obtainMessage(MSG_SCHEDULE_OF_PROGRAMS, Pair.create(mChannel, mPrograms))
                     .sendToTarget();
         }
         mHandler.removeMessages(MSG_RESCHEDULE_PROGRAMS);
@@ -1966,10 +2036,12 @@
     private void doDiscoverCaptionServiceNumber(int serviceNumber) {
         int index = mCaptionTrackMap.indexOfKey(serviceNumber);
         if (index < 0) {
-            AtscCaptionTrack captionTrack = new AtscCaptionTrack();
-            captionTrack.serviceNumber = serviceNumber;
-            captionTrack.wideAspectRatio = false;
-            captionTrack.easyReader = false;
+            AtscCaptionTrack captionTrack =
+                    AtscCaptionTrack.newBuilder()
+                            .setServiceNumber(serviceNumber)
+                            .setWideAspectRatio(false)
+                            .setEasyReader(false)
+                            .build();
             mCaptionTrackMap.put(serviceNumber, captionTrack);
             mTvTracks.add(
                     new TvTrackInfo.Builder(
@@ -1988,7 +2060,7 @@
         ImmutableList<TvContentRating> ratings =
                 mTvContentRatingCache.getRatings(currentProgram.getContentRating());
         if ((ratings == null || ratings.isEmpty())) {
-            if (Experiments.ENABLE_UNRATED_CONTENT_SETTINGS.get()) {
+            if (mLegacyFlags.enableUnratedContentSettings()) {
                 ratings = ImmutableList.of(TvContentRating.UNRATED);
             } else {
                 ratings = NO_CONTENT_RATINGS;
diff --git a/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorkerExoV2.java b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorkerExoV2.java
index 82afff1..f8a87d8 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorkerExoV2.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/TunerSessionWorkerExoV2.java
@@ -44,22 +44,22 @@
 import android.util.SparseArray;
 import android.view.Surface;
 import android.view.accessibility.CaptioningManager;
+
 import com.android.tv.common.CommonPreferences.TrickplaySetting;
 import com.android.tv.common.SoftPreconditions;
 import com.android.tv.common.TvContentRatingCache;
 import com.android.tv.common.compat.TvInputConstantCompat;
 import com.android.tv.common.customization.CustomizationManager;
 import com.android.tv.common.customization.CustomizationManager.TRICKPLAY_MODE;
-import com.android.tv.common.experiments.Experiments;
+import com.android.tv.common.dev.DeveloperPreferences;
 import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.common.util.SystemPropertiesProxy;
 import com.android.tv.tuner.data.Cea708Data;
+import com.android.tv.tuner.data.Channel;
 import com.android.tv.tuner.data.PsipData.EitItem;
 import com.android.tv.tuner.data.PsipData.TvTracksInterface;
+import com.android.tv.tuner.data.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
 import com.android.tv.tuner.data.TunerChannel;
-import com.android.tv.tuner.data.nano.Channel;
-import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
 import com.android.tv.tuner.exoplayer.MpegTsPlayer;
 import com.android.tv.tuner.exoplayer.MpegTsRendererBuilder;
 import com.android.tv.tuner.exoplayer.buffer.BufferManager;
@@ -74,10 +74,16 @@
 import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
 import com.android.tv.tuner.tvinput.debug.TunerDebug;
 import com.android.tv.tuner.util.StatusTextUtils;
+
 import com.google.android.exoplayer.ExoPlayer;
 import com.google.android.exoplayer.audio.AudioCapabilities;
+import com.google.auto.factory.AutoFactory;
+import com.google.auto.factory.Provided;
 import com.google.common.collect.ImmutableList;
+
 import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
+import com.android.tv.common.flags.LegacyFlags;
+
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Iterator;
@@ -100,8 +106,6 @@
     private static final boolean DEBUG = false;
     private static final boolean ENABLE_PROFILER = true;
     private static final String PLAY_FROM_CHANNEL = "channel";
-    private static final String MAX_BUFFER_SIZE_KEY = "tv.tuner.buffersize_mbytes";
-    private static final int MAX_BUFFER_SIZE_DEF = 2 * 1024; // 2GB
     private static final int MIN_BUFFER_SIZE_DEF = 256; // 256MB
 
     // Public messages
@@ -231,17 +235,37 @@
     private boolean mReleaseRequested; // Guarded by mReleaseLock
     private final Object mReleaseLock = new Object();
     private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
+    private final LegacyFlags mLegacyFlags;
+    private Uri mChannelUri;
+    private Uri mRecordingUri;
+    private boolean mOnTuneUsesRecording = false;
 
     private int mSignalStrength;
     private long mRecordedProgramStartTimeMs;
 
+    /**
+     * Factory for {@link TunerSessionWorkerExoV2}.
+     *
+     * <p>This wrapper class keeps other classes from needing to reference the {@link AutoFactory}
+     * generated class.
+     */
+    public interface Factory {
+        public TunerSessionWorkerExoV2 create(
+                Context context,
+                ChannelDataManager channelDataManager,
+                TunerSessionExoV2 tunerSession,
+                TunerSessionOverlay tunerSessionOverlay);
+    }
+
+    @AutoFactory(implementing = TunerSessionWorkerExoV2.Factory.class)
     public TunerSessionWorkerExoV2(
             Context context,
             ChannelDataManager channelDataManager,
             TunerSessionExoV2 tunerSession,
             TunerSessionOverlay tunerSessionOverlay,
-            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
-            TsDataSourceManager.Factory tsDataSourceManagerFactory) {
+            @Provided ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
+            @Provided LegacyFlags legacyFlags,
+            @Provided TsDataSourceManager.Factory tsDataSourceManagerFactory) {
         this(
                 context,
                 channelDataManager,
@@ -249,6 +273,7 @@
                 tunerSessionOverlay,
                 null,
                 concurrentDvrPlaybackFlags,
+                legacyFlags,
                 tsDataSourceManagerFactory);
     }
 
@@ -260,8 +285,10 @@
             TunerSessionOverlay tunerSessionOverlay,
             @Nullable Handler handler,
             ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
+            LegacyFlags legacyFlags,
             TsDataSourceManager.Factory tsDataSourceManagerFactory) {
         mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
+        mLegacyFlags = legacyFlags;
         if (DEBUG) {
             Log.d(TAG, "TunerSessionWorkerExoV2 created");
         }
@@ -278,6 +305,7 @@
         mSession = tunerSession;
         mTunerSessionOverlay = tunerSessionOverlay;
         mChannelDataManager = channelDataManager;
+        mRecordingUri = null;
         mChannelDataManager.setListener(this);
         mChannelDataManager.checkDataVersion(mContext);
         mSourceManager = tsDataSourceManagerFactory.create(false);
@@ -294,8 +322,7 @@
                 (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
         mCaptionEnabled = captioningManager.isEnabled();
         mPlaybackParams.setSpeed(1.0f);
-        mMaxTrickplayBufferSizeMb =
-                SystemPropertiesProxy.getInt(MAX_BUFFER_SIZE_KEY, MAX_BUFFER_SIZE_DEF);
+        mMaxTrickplayBufferSizeMb = DeveloperPreferences.MAX_BUFFER_SIZE_MBYTES.get(context);
         mTrickplayModeCustomization = CustomizationManager.getTrickplayMode(context);
         if (mTrickplayModeCustomization
                 == CustomizationManager.TRICKPLAY_MODE_USE_EXTERNAL_STORAGE) {
@@ -493,6 +520,11 @@
             // Final status
             // notification of STATE_ENDED from MpegTsPlayer will be ignored afterwards.
             Log.i(TAG, "Player ended: end of stream");
+            if (mOnTuneUsesRecording) {
+                mRecordingUri = null;
+                mSession.notifyChannelRetuned(mChannelUri);
+                sendMessage(MSG_TUNE, mChannelUri);
+            }
             if (mChannel != null) {
                 sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer));
             }
@@ -521,10 +553,10 @@
     @Override
     public void onVideoSizeChanged(int width, int height, float pixelWidthHeight) {
         if (mChannel != null && mChannel.hasVideo()) {
-            updateVideoTrack(width, height);
+            updateVideoTrack(width, height, pixelWidthHeight);
         }
         if (mRecordingId != null) {
-            updateVideoTrack(width, height);
+            updateVideoTrack(width, height, pixelWidthHeight);
         }
     }
 
@@ -540,6 +572,9 @@
             } else {
                 mBufferStartTimeMs = mRecordStartTimeMs = System.currentTimeMillis();
             }
+            if (mOnTuneUsesRecording) {
+                mBufferStartTimeMs = mRecordStartTimeMs = mRecordedProgramStartTimeMs;
+            }
             notifyVideoAvailable();
             mReportedDrawnToSurface = true;
 
@@ -595,7 +630,7 @@
     // ChannelDataManager.ProgramInfoListener
     @Override
     public void onProgramsArrived(TunerChannel channel, List<EitItem> programs) {
-        sendMessage(MSG_SCHEDULE_OF_PROGRAMS, new Pair<>(channel, programs));
+        sendMessage(MSG_SCHEDULE_OF_PROGRAMS, Pair.create(channel, programs));
     }
 
     @Override
@@ -610,7 +645,7 @@
 
     @Override
     public void onRequestProgramsResponse(TunerChannel channel, List<EitItem> programs) {
-        sendMessage(MSG_PROGRAM_DATA_RESULT, new Pair<>(channel, programs));
+        sendMessage(MSG_PROGRAM_DATA_RESULT, Pair.create(channel, programs));
     }
 
     // PlaybackBufferListener
@@ -658,7 +693,7 @@
     }
 
     private static class RecordedProgram {
-        //        private final long mChannelId;
+        private final long mChannelId;
         private final String mDataUri;
         private final long mStartTimeMillis;
 
@@ -670,14 +705,13 @@
 
         public RecordedProgram(Cursor cursor) {
             int index = 0;
-            //            mChannelId = cursor.getLong(index++);
-            index++;
+            mChannelId = cursor.getLong(index++);
             mDataUri = cursor.getString(index++);
             mStartTimeMillis = cursor.getLong(index++);
         }
 
         public RecordedProgram(long channelId, String dataUri) {
-            //            mChannelId = channelId;
+            mChannelId = channelId;
             mDataUri = dataUri;
             mStartTimeMillis = 0;
         }
@@ -697,6 +731,10 @@
         public long getStartTime() {
             return mStartTimeMillis;
         }
+
+        public long getChannelId() {
+            return mChannelId;
+        }
     }
 
     private RecordedProgram getRecordedProgram(Uri recordedUri) {
@@ -721,9 +759,13 @@
         }
     }
 
-    private String parseRecording(Uri uri) {
+    private String parseRecording(Uri uri, long channelId) {
         RecordedProgram recording = getRecordedProgram(uri);
         if (recording != null) {
+            if (channelId != -1 && channelId != recording.getChannelId()) {
+                // Recorded URI is of some other channel
+                return null;
+            }
             mRecordedProgramStartTimeMs = recording.getStartTime();
             return recording.getDataUri();
         }
@@ -836,10 +878,19 @@
             mIsActiveSession = true;
         }
         String recording = null;
+        mOnTuneUsesRecording = false;
         long channelId = parseChannel(channelUri);
         TunerChannel channel = (channelId == -1) ? null : mChannelDataManager.getChannel(channelId);
+        mRecordingUri = mSession.getRecordingUri(channelUri);
         if (channelId == -1) {
-            recording = parseRecording(channelUri);
+            recording = parseRecording(channelUri, channelId);
+        } else if (mRecordingUri != null && mConcurrentDvrPlaybackFlags.onTuneUsesRecording()) {
+            mChannelUri = channelUri;
+            recording = parseRecording(mRecordingUri, channelId);
+            if (recording != null) {
+                mOnTuneUsesRecording = true;
+                channel = null;
+            }
         }
         if (channel == null && recording == null) {
             Log.w(TAG, "onTune() is failed. Can't find channel for " + channelUri);
@@ -1442,8 +1493,7 @@
         }
         MpegTsPlayer player =
                 new MpegTsPlayer(
-                        new MpegTsRendererBuilder(
-                                mContext, bufferManager, this, mConcurrentDvrPlaybackFlags),
+                        new MpegTsRendererBuilder(mContext, bufferManager, this),
                         mHandler,
                         mSourceManager,
                         capabilities,
@@ -1456,7 +1506,7 @@
         player.setVideoEventListener(this);
         player.setCaptionServiceNumber(
                 mCaptionTrack != null
-                        ? mCaptionTrack.serviceNumber
+                        ? mCaptionTrack.getServiceNumber()
                         : Cea708Data.EMPTY_SERVICE_NUMBER);
         return player;
     }
@@ -1466,7 +1516,7 @@
             mTunerSessionOverlay.sendUiMessage(
                     TunerSessionOverlay.MSG_UI_START_CAPTION_TRACK, mCaptionTrack);
             if (mPlayer != null) {
-                mPlayer.setCaptionServiceNumber(mCaptionTrack.serviceNumber);
+                mPlayer.setCaptionServiceNumber(mCaptionTrack.getServiceNumber());
             }
         }
     }
@@ -1523,12 +1573,13 @@
         }
     }
 
-    private void updateVideoTrack(int width, int height) {
+    private void updateVideoTrack(int width, int height, float pixelWidthHeight) {
         removeTvTracks(TvTrackInfo.TYPE_VIDEO);
         mTvTracks.add(
                 new TvTrackInfo.Builder(TvTrackInfo.TYPE_VIDEO, VIDEO_TRACK_ID)
                         .setVideoWidth(width)
                         .setVideoHeight(height)
+                        .setVideoPixelAspectRatio(pixelWidthHeight)
                         .build());
         mSession.notifyTracksChanged(mTvTracks);
         mSession.notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, VIDEO_TRACK_ID);
@@ -1542,7 +1593,7 @@
         if (audioTracks != null) {
             int index = 0;
             for (AtscAudioTrack audioTrack : audioTracks) {
-                audioTrack.index = index;
+                audioTrack = audioTrack.toBuilder().setIndex(index).build();
                 mAudioTrackMap.put(index, audioTrack);
                 ++index;
             }
@@ -1572,10 +1623,10 @@
             String language =
                     !TextUtils.isEmpty(infoFromPlayer.language)
                             ? infoFromPlayer.language
-                            : (infoFromEit != null && infoFromEit.language != null)
-                                    ? infoFromEit.language
-                                    : (infoFromVct != null && infoFromVct.language != null)
-                                            ? infoFromVct.language
+                            : (infoFromEit != null && infoFromEit.hasLanguage())
+                                    ? infoFromEit.getLanguage()
+                                    : (infoFromVct != null && infoFromVct.hasLanguage())
+                                            ? infoFromVct.getLanguage()
                                             : null;
             TvTrackInfo.Builder builder =
                     new TvTrackInfo.Builder(TvTrackInfo.TYPE_AUDIO, AUDIO_TRACK_PREFIX + i);
@@ -1596,20 +1647,20 @@
         mCaptionTrackMap.clear();
         if (captionTracks != null) {
             for (AtscCaptionTrack captionTrack : captionTracks) {
-                if (mCaptionTrackMap.indexOfKey(captionTrack.serviceNumber) >= 0) {
+                if (mCaptionTrackMap.indexOfKey(captionTrack.getServiceNumber()) >= 0) {
                     continue;
                 }
-                String language = captionTrack.language;
+                String language = captionTrack.getLanguage();
 
                 // The service number of the caption service is used for track id of a subtitle.
                 // Later, when a subtitle is chosen, track id will be passed on to TsParser.
                 TvTrackInfo.Builder builder =
                         new TvTrackInfo.Builder(
                                 TvTrackInfo.TYPE_SUBTITLE,
-                                SUBTITLE_TRACK_PREFIX + captionTrack.serviceNumber);
+                                SUBTITLE_TRACK_PREFIX + captionTrack.getServiceNumber());
                 builder.setLanguage(language);
                 mTvTracks.add(builder.build());
-                mCaptionTrackMap.put(captionTrack.serviceNumber, captionTrack);
+                mCaptionTrackMap.put(captionTrack.getServiceNumber(), captionTrack);
             }
         }
         mSession.notifyTracksChanged(mTvTracks);
@@ -1791,6 +1842,9 @@
         } else {
             mBufferStartTimeMs = mRecordStartTimeMs = System.currentTimeMillis();
         }
+        if (mOnTuneUsesRecording) {
+            mBufferStartTimeMs = mRecordStartTimeMs = mRecordedProgramStartTimeMs;
+        }
         mLastPositionMs = 0;
         mCaptionTrack = null;
         mSignalStrength = TvInputConstantCompat.SIGNAL_STRENGTH_UNKNOWN;
@@ -1798,6 +1852,14 @@
             mSession.notifySignalStrength(mSignalStrength);
         }
         mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
+        if (mOnTuneUsesRecording) {
+            mHandler.obtainMessage(
+                            MSG_TIMESHIFT_SEEK_TO,
+                            1,
+                            0,
+                            System.currentTimeMillis() - SEEK_MARGIN_MS)
+                    .sendToTarget();
+        }
     }
 
     private void doReschedulePrograms() {
@@ -1819,7 +1881,7 @@
                                 + " current program: "
                                 + getCurrentProgram());
             }
-            mHandler.obtainMessage(MSG_SCHEDULE_OF_PROGRAMS, new Pair<>(mChannel, mPrograms))
+            mHandler.obtainMessage(MSG_SCHEDULE_OF_PROGRAMS, Pair.create(mChannel, mPrograms))
                     .sendToTarget();
         }
         mHandler.removeMessages(MSG_RESCHEDULE_PROGRAMS);
@@ -1980,10 +2042,13 @@
     private void doDiscoverCaptionServiceNumber(int serviceNumber) {
         int index = mCaptionTrackMap.indexOfKey(serviceNumber);
         if (index < 0) {
-            AtscCaptionTrack captionTrack = new AtscCaptionTrack();
-            captionTrack.serviceNumber = serviceNumber;
-            captionTrack.wideAspectRatio = false;
-            captionTrack.easyReader = false;
+            AtscCaptionTrack.Builder captionTrackBuilder = AtscCaptionTrack.newBuilder();
+            AtscCaptionTrack captionTrack =
+                    captionTrackBuilder
+                            .setServiceNumber(serviceNumber)
+                            .setWideAspectRatio(false)
+                            .setEasyReader(false)
+                            .build();
             mCaptionTrackMap.put(serviceNumber, captionTrack);
             mTvTracks.add(
                     new TvTrackInfo.Builder(
@@ -2002,7 +2067,7 @@
         ImmutableList<TvContentRating> ratings =
                 mTvContentRatingCache.getRatings(currentProgram.getContentRating());
         if ((ratings == null || ratings.isEmpty())) {
-            if (Experiments.ENABLE_UNRATED_CONTENT_SETTINGS.get()) {
+            if (mLegacyFlags.enableUnratedContentSettings()) {
                 ratings = ImmutableList.of(TvContentRating.UNRATED);
             } else {
                 ratings = NO_CONTENT_RATINGS;
diff --git a/tuner/src/com/android/tv/tuner/tvinput/datamanager/ChannelDataManager.java b/tuner/src/com/android/tv/tuner/tvinput/datamanager/ChannelDataManager.java
index 585b28b..447618a 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/datamanager/ChannelDataManager.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/datamanager/ChannelDataManager.java
@@ -29,11 +29,10 @@
 import android.os.HandlerThread;
 import android.os.Message;
 import android.os.RemoteException;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.text.format.DateUtils;
 import android.util.Log;
-import com.android.tv.common.singletons.HasSingletons;
-import com.android.tv.common.singletons.HasTvInputId;
 import com.android.tv.common.util.PermissionUtils;
 import com.android.tv.tuner.data.PsipData.EitItem;
 import com.android.tv.tuner.data.TunerChannel;
@@ -51,7 +50,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 
-/** Manages the channel info and EPG data through {@link TvInputManager}. */
+/** Manages the channel info and EPG data for a specific inputId. */
 public class ChannelDataManager implements Handler.Callback {
     private static final String TAG = "ChannelDataManager";
 
@@ -146,9 +145,9 @@
         void onChannelHandlingDone();
     }
 
-    public ChannelDataManager(Context context) {
+    public ChannelDataManager(Context context, String inputId) {
         mContext = context;
-        mInputId = HasSingletons.get(HasTvInputId.class, context).getEmbeddedTunerInputId();
+        mInputId = inputId;
         mChannelsUri = TvContract.buildChannelsUriForInput(mInputId);
         mTunerChannelMap = new ConcurrentHashMap<>();
         mTunerChannelIdMap = new ConcurrentSkipListMap<>();
@@ -382,6 +381,12 @@
         return false;
     }
 
+    @NonNull
+    @Override
+    public String toString() {
+        return "ChannelDataManager[" + mInputId + "]";
+    }
+
     // Private methods
     private void handleEvents(TunerChannel channel, List<EitItem> items) {
         long channelId = getChannelId(channel);
diff --git a/tuner/src/com/android/tv/tuner/tvinput/factory/TunerRecordingSessionFactory.java b/tuner/src/com/android/tv/tuner/tvinput/factory/TunerRecordingSessionFactory.java
new file mode 100644
index 0000000..c595075
--- /dev/null
+++ b/tuner/src/com/android/tv/tuner/tvinput/factory/TunerRecordingSessionFactory.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.tuner.tvinput.factory;
+
+import android.media.tv.TvInputService.RecordingSession;
+
+import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
+
+/** {@link RecordingSession} factory */
+public interface TunerRecordingSessionFactory {
+
+    /** Called when a recording session is released */
+    interface RecordingSessionReleasedCallback {
+
+        /**
+         * Called when the given recording session is released.
+         *
+         * @param session The recording session that has been released.
+         */
+        void onReleased(RecordingSession session);
+    }
+
+    RecordingSession create(
+            String inputId,
+            RecordingSessionReleasedCallback releasedCallback,
+            ChannelDataManager channelDataManager);
+}
diff --git a/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactory.java b/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactory.java
index a27cb22..e22562a 100644
--- a/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactory.java
+++ b/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactory.java
@@ -1,7 +1,24 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package com.android.tv.tuner.tvinput.factory;
 
-import android.content.Context;
 import android.media.tv.TvInputService.Session;
+import android.net.Uri;
+
 import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
 
 /** {@link android.media.tv.TvInputService.Session} factory */
@@ -18,8 +35,19 @@
         void onReleased(Session session);
     }
 
+    /** Called when recording URI is required for playback */
+    interface SessionRecordingCallback {
+
+        /**
+         * Called when recording URI is required for playback.
+         *
+         * @param channelUri for which recording URI is requested.
+         */
+        Uri getRecordingUri(Uri channelUri);
+    }
+
     Session create(
-            Context context,
             ChannelDataManager channelDataManager,
-            SessionReleasedCallback releasedCallback);
+            SessionReleasedCallback releasedCallback,
+            SessionRecordingCallback recordingCallback);
 }
diff --git a/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactoryImpl.java b/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactoryImpl.java
deleted file mode 100644
index 54e959e..0000000
--- a/tuner/src/com/android/tv/tuner/tvinput/factory/TunerSessionFactoryImpl.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package com.android.tv.tuner.tvinput.factory;
-
-import android.content.Context;
-import android.media.tv.TvInputService.Session;
-import com.android.tv.tuner.source.TsDataSourceManager;
-import com.android.tv.tuner.tvinput.TunerSession;
-import com.android.tv.tuner.tvinput.TunerSessionExoV2;
-import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
-import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags;
-import com.android.tv.common.flags.TunerFlags;
-import javax.inject.Inject;
-
-/** Creates a {@link TunerSessionFactory}. */
-public class TunerSessionFactoryImpl implements TunerSessionFactory {
-
-    private final TunerFlags mTunerFlags;
-    private final ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
-    private final TsDataSourceManager.Factory mTsDataSourceManagerFactory;
-
-    @Inject
-    public TunerSessionFactoryImpl(
-            TunerFlags tunerFlags,
-            ConcurrentDvrPlaybackFlags concurrentDvrPlaybackFlags,
-            TsDataSourceManager.Factory tsDataSourceManagerFactory) {
-        mTunerFlags = tunerFlags;
-        mConcurrentDvrPlaybackFlags = concurrentDvrPlaybackFlags;
-        mTsDataSourceManagerFactory = tsDataSourceManagerFactory;
-    }
-
-    @Override
-    public Session create(
-            Context context,
-            ChannelDataManager channelDataManager,
-            SessionReleasedCallback releasedCallback) {
-        return mTunerFlags.useExoplayerV2()
-                ? new TunerSessionExoV2(
-                        context,
-                        channelDataManager,
-                        releasedCallback,
-                        mConcurrentDvrPlaybackFlags,
-                        mTsDataSourceManagerFactory)
-                : new TunerSession(
-                        context,
-                        channelDataManager,
-                        releasedCallback,
-                        mConcurrentDvrPlaybackFlags,
-                        mTsDataSourceManagerFactory);
-    }
-}
diff --git a/tuner/tests/robotests/javatests/com/android/tv/tuner/data/SectionParserTest.java b/tuner/tests/robotests/javatests/com/android/tv/tuner/data/SectionParserTest.java
new file mode 100644
index 0000000..5c5e32a
--- /dev/null
+++ b/tuner/tests/robotests/javatests/com/android/tv/tuner/data/SectionParserTest.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.data;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import com.android.tv.testing.constants.ConfigConstants;
+import com.android.tv.tuner.data.PsipData.ContentAdvisoryDescriptor;
+import com.android.tv.tuner.data.PsipData.RatingRegion;
+import com.android.tv.tuner.data.PsipData.RegionalRating;
+import com.android.tv.tuner.data.PsipData.TsDescriptor;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link com.android.tv.tuner.data.SectionParser}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class SectionParserTest {
+    private static final Map<String, String> US_RATING_MAP = new HashMap<>();
+    private static final int RATING_REGION_US = 1;
+
+    static {
+        // These mappings are from table 3 of ANSI-CEA-766-D
+        US_RATING_MAP.put("1 0 0 0 0 0 0 X", ""); // TV-None
+        US_RATING_MAP.put("0 0 0 0 0 1 0 X", "com.android.tv/US_TV/US_TV_Y");
+        US_RATING_MAP.put("0 0 0 0 0 2 0 X", "com.android.tv/US_TV/US_TV_Y7");
+        US_RATING_MAP.put("0 0 0 0 0 2 1 X", "com.android.tv/US_TV/US_TV_Y7/US_TV_FV");
+        US_RATING_MAP.put("2 0 0 0 0 0 0 X", "com.android.tv/US_TV/US_TV_G");
+        US_RATING_MAP.put("3 0 0 0 0 0 0 X", "com.android.tv/US_TV/US_TV_PG");
+        US_RATING_MAP.put("3 1 0 0 0 0 0 X", "com.android.tv/US_TV/US_TV_PG/US_TV_D");
+        US_RATING_MAP.put("3 0 1 0 0 0 0 X", "com.android.tv/US_TV/US_TV_PG/US_TV_L");
+        US_RATING_MAP.put("3 0 0 1 0 0 0 X", "com.android.tv/US_TV/US_TV_PG/US_TV_S");
+        US_RATING_MAP.put("3 0 0 0 1 0 0 X", "com.android.tv/US_TV/US_TV_PG/US_TV_V");
+        US_RATING_MAP.put("3 1 1 0 0 0 0 X", "com.android.tv/US_TV/US_TV_PG/US_TV_D/US_TV_L");
+        US_RATING_MAP.put("3 1 0 1 0 0 0 X", "com.android.tv/US_TV/US_TV_PG/US_TV_D/US_TV_S");
+        US_RATING_MAP.put("3 1 0 0 1 0 0 X", "com.android.tv/US_TV/US_TV_PG/US_TV_D/US_TV_V");
+        US_RATING_MAP.put("3 0 1 1 0 0 0 X", "com.android.tv/US_TV/US_TV_PG/US_TV_L/US_TV_S");
+        US_RATING_MAP.put("3 0 1 0 1 0 0 X", "com.android.tv/US_TV/US_TV_PG/US_TV_L/US_TV_V");
+        US_RATING_MAP.put("3 0 0 1 1 0 0 X", "com.android.tv/US_TV/US_TV_PG/US_TV_S/US_TV_V");
+        US_RATING_MAP.put(
+                "3 1 1 1 0 0 0 X", "com.android.tv/US_TV/US_TV_PG/US_TV_D/US_TV_L/US_TV_S");
+        US_RATING_MAP.put(
+                "3 1 1 0 1 0 0 X", "com.android.tv/US_TV/US_TV_PG/US_TV_D/US_TV_L/US_TV_V");
+        US_RATING_MAP.put(
+                "3 1 0 1 1 0 0 X", "com.android.tv/US_TV/US_TV_PG/US_TV_D/US_TV_S/US_TV_V");
+        US_RATING_MAP.put(
+                "3 0 1 1 1 0 0 X", "com.android.tv/US_TV/US_TV_PG/US_TV_L/US_TV_S/US_TV_V");
+        US_RATING_MAP.put(
+                "3 1 1 1 1 0 0 X", "com.android.tv/US_TV/US_TV_PG/US_TV_D/US_TV_L/US_TV_S/US_TV_V");
+        US_RATING_MAP.put("4 0 0 0 0 0 0 X", "com.android.tv/US_TV/US_TV_14");
+        US_RATING_MAP.put("4 1 0 0 0 0 0 X", "com.android.tv/US_TV/US_TV_14/US_TV_D");
+        US_RATING_MAP.put("4 0 1 0 0 0 0 X", "com.android.tv/US_TV/US_TV_14/US_TV_L");
+        US_RATING_MAP.put("4 0 0 1 0 0 0 X", "com.android.tv/US_TV/US_TV_14/US_TV_S");
+        US_RATING_MAP.put("4 0 0 0 1 0 0 X", "com.android.tv/US_TV/US_TV_14/US_TV_V");
+        US_RATING_MAP.put("4 1 1 0 0 0 0 X", "com.android.tv/US_TV/US_TV_14/US_TV_D/US_TV_L");
+        US_RATING_MAP.put("4 1 0 1 0 0 0 X", "com.android.tv/US_TV/US_TV_14/US_TV_D/US_TV_S");
+        US_RATING_MAP.put("4 1 0 0 1 0 0 X", "com.android.tv/US_TV/US_TV_14/US_TV_D/US_TV_V");
+        US_RATING_MAP.put("4 0 1 1 0 0 0 X", "com.android.tv/US_TV/US_TV_14/US_TV_L/US_TV_S");
+        US_RATING_MAP.put("4 0 1 0 1 0 0 X", "com.android.tv/US_TV/US_TV_14/US_TV_L/US_TV_V");
+        US_RATING_MAP.put("4 0 0 1 1 0 0 X", "com.android.tv/US_TV/US_TV_14/US_TV_S/US_TV_V");
+        US_RATING_MAP.put(
+                "4 1 1 1 0 0 0 X", "com.android.tv/US_TV/US_TV_14/US_TV_D/US_TV_L/US_TV_S");
+        US_RATING_MAP.put(
+                "4 1 1 0 1 0 0 X", "com.android.tv/US_TV/US_TV_14/US_TV_D/US_TV_L/US_TV_V");
+        US_RATING_MAP.put(
+                "4 1 0 1 1 0 0 X", "com.android.tv/US_TV/US_TV_14/US_TV_D/US_TV_S/US_TV_V");
+        US_RATING_MAP.put(
+                "4 0 1 1 1 0 0 X", "com.android.tv/US_TV/US_TV_14/US_TV_L/US_TV_S/US_TV_V");
+        US_RATING_MAP.put(
+                "4 1 1 1 1 0 0 X", "com.android.tv/US_TV/US_TV_14/US_TV_D/US_TV_L/US_TV_S/US_TV_V");
+        US_RATING_MAP.put("5 0 0 0 0 0 0 X", "com.android.tv/US_TV/US_TV_MA");
+        US_RATING_MAP.put("5 0 1 0 0 0 0 X", "com.android.tv/US_TV/US_TV_MA/US_TV_L");
+        US_RATING_MAP.put("5 0 0 1 0 0 0 X", "com.android.tv/US_TV/US_TV_MA/US_TV_S");
+        US_RATING_MAP.put("5 0 0 0 1 0 0 X", "com.android.tv/US_TV/US_TV_MA/US_TV_V");
+        US_RATING_MAP.put("5 0 1 1 0 0 0 X", "com.android.tv/US_TV/US_TV_MA/US_TV_L/US_TV_S");
+        US_RATING_MAP.put("5 0 1 0 1 0 0 X", "com.android.tv/US_TV/US_TV_MA/US_TV_L/US_TV_V");
+        US_RATING_MAP.put("5 0 0 1 1 0 0 X", "com.android.tv/US_TV/US_TV_MA/US_TV_S/US_TV_V");
+        US_RATING_MAP.put(
+                "5 0 1 1 1 0 0 X", "com.android.tv/US_TV/US_TV_MA/US_TV_L/US_TV_S/US_TV_V");
+        US_RATING_MAP.put("X X X X X X X 1", ""); // MPAA-N/A
+        US_RATING_MAP.put("X X X X X X X 2", "com.android.tv/US_MV/US_MV_G");
+        US_RATING_MAP.put("X X X X X X X 3", "com.android.tv/US_MV/US_MV_PG");
+        US_RATING_MAP.put("X X X X X X X 4", "com.android.tv/US_MV/US_MV_PG13");
+        US_RATING_MAP.put("X X X X X X X 5", "com.android.tv/US_MV/US_MV_R");
+        US_RATING_MAP.put("X X X X X X X 6", "com.android.tv/US_MV/US_MV_NC17");
+        // MPAA-X was replaced by NC17
+        US_RATING_MAP.put("X X X X X X X 7", "com.android.tv/US_MV/US_MV_NC17");
+        US_RATING_MAP.put("X X X X X X X 8", ""); // MPAA - Not Rated
+    }
+
+    @Test
+    public void testGenerateContentRating_emptyInput() {
+        assertThat(SectionParser.generateContentRating(new ArrayList<TsDescriptor>())).isEmpty();
+    }
+
+    @Test
+    public void testGenerateContentRating_validInputs() {
+        for (Map.Entry<String, String> entry : US_RATING_MAP.entrySet()) {
+            RatingRegion ratingRegion = createRatingRegionForTest(entry.getKey(), RATING_REGION_US);
+            ContentAdvisoryDescriptor descriptor = createDescriptorForTest(ratingRegion);
+            assertWithMessage("key = " + entry.getKey())
+                    .that(
+                            SectionParser.generateContentRating(
+                                    Collections.singletonList((TsDescriptor) descriptor)))
+                    .isEqualTo(entry.getValue());
+        }
+    }
+
+    @Test
+    public void testGenerateContentRating_invalidInput() {
+        // Invalid because the value of the first dimension is lost.
+        RatingRegion ratingRegion = createRatingRegionForTest("X 1 0 0 0 0 0 X", RATING_REGION_US);
+        ContentAdvisoryDescriptor descriptor = createDescriptorForTest(ratingRegion);
+        assertThat(
+                        SectionParser.generateContentRating(
+                                Collections.singletonList((TsDescriptor) descriptor)))
+                .isEmpty();
+    }
+
+    @Test
+    public void testGenerateContentRating_multipleRatings() {
+        // TV-MA
+        RatingRegion ratingRegionTv =
+                createRatingRegionForTest("5 0 0 0 0 0 0 X", RATING_REGION_US);
+        // MPAA-R
+        RatingRegion ratingRegionMv =
+                createRatingRegionForTest("X X X X X X X 5", RATING_REGION_US);
+        ContentAdvisoryDescriptor descriptorTv = createDescriptorForTest(ratingRegionTv);
+        ContentAdvisoryDescriptor descriptorMv = createDescriptorForTest(ratingRegionMv);
+        assertThat(
+                        SectionParser.generateContentRating(
+                                Arrays.<TsDescriptor>asList(descriptorTv, descriptorMv)))
+                .isEqualTo("com.android.tv/US_MV/US_MV_R,com.android.tv/US_TV/US_TV_MA");
+    }
+
+    private static RatingRegion createRatingRegionForTest(String values, int region) {
+        String[] valueArray = values.split(" ");
+        List<RegionalRating> regionalRatings = new ArrayList<>();
+        for (int i = 0; i < valueArray.length; i++) {
+            try {
+                int value = Integer.valueOf(valueArray[i]);
+                if (value != 0) {
+                    // value 0 means the dimension should be omitted from the descriptor
+                    regionalRatings.add(new RegionalRating(i, value));
+                }
+            } catch (NumberFormatException e) {
+                // do nothing
+            }
+        }
+        return new RatingRegion(region, "", regionalRatings);
+    }
+
+    private static ContentAdvisoryDescriptor createDescriptorForTest(RatingRegion... regions) {
+        return new ContentAdvisoryDescriptor(Arrays.asList(regions));
+    }
+}
diff --git a/tuner/tests/robotests/javatests/com/android/tv/tuner/dvb/DvbTunerHalTest.java b/tuner/tests/robotests/javatests/com/android/tv/tuner/dvb/DvbTunerHalTest.java
new file mode 100644
index 0000000..f26a4f1
--- /dev/null
+++ b/tuner/tests/robotests/javatests/com/android/tv/tuner/dvb/DvbTunerHalTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.dvb;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.tv.common.compat.TvInputConstantCompat;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.android.tv.tuner.tvinput.TunerSessionWorker;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link TunerSessionWorker}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK, application = TestSingletonApp.class)
+public class DvbTunerHalTest {
+    private int mSignal = 0;
+
+    DvbTunerHal mDvbTunerHal =
+            new DvbTunerHal(RuntimeEnvironment.application) {
+                @Override
+                protected int nativeGetSignalStrength(long deviceId) {
+                    return mSignal;
+                }
+            };
+
+    @Test
+    public void getSignalStrength_notUsed() {
+        mSignal = -3;
+        int signal = mDvbTunerHal.getSignalStrength();
+        assertThat(signal).isEqualTo(TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED);
+    }
+
+    @Test
+    public void getSignalStrength_errorMax() {
+        mSignal = Integer.MAX_VALUE;
+        int signal = mDvbTunerHal.getSignalStrength();
+        assertThat(signal).isEqualTo(TvInputConstantCompat.SIGNAL_STRENGTH_ERROR);
+    }
+
+    @Test
+    public void getSignalStrength_errorMin() {
+        mSignal = Integer.MIN_VALUE;
+        int signal = mDvbTunerHal.getSignalStrength();
+        assertThat(signal).isEqualTo(TvInputConstantCompat.SIGNAL_STRENGTH_ERROR);
+    }
+
+    @Test
+    public void getSignalStrength_error() {
+        mSignal = -1;
+        int signal = mDvbTunerHal.getSignalStrength();
+        assertThat(signal).isEqualTo(TvInputConstantCompat.SIGNAL_STRENGTH_ERROR);
+    }
+
+    @Test
+    public void getSignalStrength_curvedMax() {
+        mSignal = 65535;
+        int signal = mDvbTunerHal.getSignalStrength();
+        assertThat(signal).isEqualTo(100);
+    }
+
+    @Test
+    public void getSignalStrength_curvedHalf() {
+        mSignal = 58982;
+        int signal = mDvbTunerHal.getSignalStrength();
+        assertThat(signal).isEqualTo(50);
+    }
+
+    @Test
+    public void getSignalStrength_curvedMin() {
+        mSignal = 0;
+        int signal = mDvbTunerHal.getSignalStrength();
+        assertThat(signal).isEqualTo(0);
+    }
+}
diff --git a/tuner/tests/robotests/javatests/com/android/tv/tuner/exoplayer/tests/AssetDataSource.java b/tuner/tests/robotests/javatests/com/android/tv/tuner/exoplayer/tests/AssetDataSource.java
new file mode 100644
index 0000000..52faa1d
--- /dev/null
+++ b/tuner/tests/robotests/javatests/com/android/tv/tuner/exoplayer/tests/AssetDataSource.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *            http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.exoplayer.tests;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.net.Uri;
+import com.google.android.exoplayer.C;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.TransferListener;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+
+/** A local asset. */
+// Copied from com.google.android.exoplayer.upstream.AssetDataSource for test.
+final class AssetDataSource implements DataSource {
+    /** Thrown when an {@link IOException} is encountered reading a local asset. */
+    private static final class AssetDataSourceException extends IOException {
+        private AssetDataSourceException(IOException cause) {
+            super(cause);
+        }
+    }
+
+    private final AssetManager mAssetManager;
+
+    private InputStream mInputStream;
+    private long mBytesRemaining;
+    private Uri mUri;
+
+    /** Constructs a new {@link DataSource} that retrieves data from a local asset. */
+    AssetDataSource(Context context) {
+        mAssetManager = context.getAssets();
+    }
+
+    @Override
+    public long open(DataSpec dataSpec) throws AssetDataSourceException {
+        try {
+            String path = dataSpec.uri.getPath();
+            if (path.startsWith("/android_asset/")) {
+                path = path.substring(15);
+            } else if (path.startsWith("/")) {
+                path = path.substring(1);
+            }
+            mInputStream = mAssetManager.open(path, AssetManager.ACCESS_RANDOM);
+            long skipped = mInputStream.skip(dataSpec.position);
+            if (skipped < dataSpec.position) {
+                // mAssetManager.open() returns an AssetInputStream, whose skip() implementation
+                // only skips fewer bytes than requested if the skip is beyond the end of the
+                // asset's data.
+                throw new EOFException();
+            }
+            if (dataSpec.length != C.LENGTH_UNBOUNDED) {
+                mBytesRemaining = dataSpec.length;
+            } else {
+                mBytesRemaining = mInputStream.available();
+                if (mBytesRemaining == Integer.MAX_VALUE) {
+                    // mAssetManager.open() returns an AssetInputStream, whose available()
+                    // implementation returns Integer.MAX_VALUE if the remaining length is greater
+                    // than (or equal to) Integer.MAX_VALUE. We don't know the true length in this
+                    // case, so treat as unbounded.
+                    mBytesRemaining = C.LENGTH_UNBOUNDED;
+                }
+            }
+        } catch (IOException e) {
+            throw new AssetDataSourceException(e);
+        }
+
+        mUri = dataSpec.uri;
+        return mBytesRemaining;
+    }
+
+    @Override
+    public int read(byte[] buffer, int offset, int readLength) throws AssetDataSourceException {
+        if (mBytesRemaining == 0) {
+            return -1;
+        } else {
+            int bytesRead = 0;
+            try {
+                int bytesToRead =
+                        mBytesRemaining == C.LENGTH_UNBOUNDED
+                                ? readLength
+                                : (int) Math.min(mBytesRemaining, readLength);
+                bytesRead = mInputStream.read(buffer, offset, bytesToRead);
+            } catch (IOException e) {
+                throw new AssetDataSourceException(e);
+            }
+
+            if (bytesRead > 0 && mBytesRemaining != C.LENGTH_UNBOUNDED) {
+                mBytesRemaining -= bytesRead;
+            }
+
+            return bytesRead;
+        }
+    }
+
+    @Override
+    public void close() throws AssetDataSourceException {
+        mUri = null;
+        if (mInputStream != null) {
+            try {
+                mInputStream.close();
+            } catch (IOException e) {
+                throw new AssetDataSourceException(e);
+            } finally {
+                mInputStream = null;
+            }
+        }
+    }
+
+    @Override
+    public void addTransferListener(TransferListener transferListener) {
+        // TODO: Implement to support metrics collection.
+    }
+
+    @Override
+    public Uri getUri() {
+        return mUri;
+    }
+}
diff --git a/tuner/tests/robotests/javatests/com/android/tv/tuner/exoplayer/tests/SampleSourceExtractorTest.java b/tuner/tests/robotests/javatests/com/android/tv/tuner/exoplayer/tests/SampleSourceExtractorTest.java
new file mode 100644
index 0000000..efafc9f
--- /dev/null
+++ b/tuner/tests/robotests/javatests/com/android/tv/tuner/exoplayer/tests/SampleSourceExtractorTest.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.exoplayer.tests;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static junit.framework.Assert.fail;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.util.Pair;
+
+import com.android.tv.testing.constants.ConfigConstants;
+import com.android.tv.tuner.exoplayer.ExoPlayerSampleExtractor;
+import com.android.tv.tuner.exoplayer.buffer.BufferManager;
+import com.android.tv.tuner.exoplayer.buffer.BufferManager.StorageManager;
+import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener;
+import com.android.tv.tuner.exoplayer.buffer.SampleChunk;
+import com.android.tv.tuner.testing.buffer.VerySlowSampleChunk;
+
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.SampleSource;
+import com.google.android.exoplayer2.upstream.DataSource;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowLooper;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.SortedMap;
+
+/** Tests for {@link ExoPlayerSampleExtractor} */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class SampleSourceExtractorTest {
+    // Maximum bandwidth of 1080p channel is about 2.2MB/s. 2MB for a sample will suffice.
+    private static final int SAMPLE_BUFFER_SIZE = 1024 * 1024 * 2;
+    private static final int CONSUMING_SAMPLES_PERIOD = 100;
+    private Uri testStreamUri;
+    private HandlerThread handlerThread;
+    private DataSource dataSource;
+
+    @Before
+    public void setUp() {
+        testStreamUri = Uri.parse("asset:///capture_stream.ts");
+        handlerThread = new HandlerThread("test");
+        dataSource = new AssetDataSource(RuntimeEnvironment.application);
+    }
+
+    @Test
+    public void testTrickplayDisabled() throws Throwable {
+        DataSource source = new AssetDataSource(RuntimeEnvironment.application);
+        MockPlaybackBufferListener listener = new MockPlaybackBufferListener();
+        ExoPlayerSampleExtractor extractor =
+                new ExoPlayerSampleExtractor(
+                        testStreamUri,
+                        source,
+                        null,
+                        listener,
+                        false,
+                        Looper.getMainLooper(),
+                        handlerThread);
+        assertWithMessage("Trickplay should be disabled").that(listener.getLastState()).isFalse();
+        // Prepares the extractor.
+        extractor.prepare();
+        // Looper is nat available until prepare is called at least once
+        Looper handlerLooper = handlerThread.getLooper();
+        try {
+            while (!extractor.prepare()) {
+
+                ShadowLooper.getShadowMainLooper().runOneTask();
+                Shadows.shadowOf(handlerLooper).runOneTask();
+            }
+        } catch (IOException e) {
+            fail("Exception occurred while preparing: " + e.getMessage());
+        }
+        // Selects all tracks.
+        List<MediaFormat> trackFormats = extractor.getTrackFormats();
+        for (int i = 0; i < trackFormats.size(); ++i) {
+            extractor.selectTrack(i);
+        }
+        // Consumes over some period.
+        SampleHolder sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
+        sampleHolder.ensureSpaceForWrite(SAMPLE_BUFFER_SIZE);
+
+        Shadows.shadowOf(handlerLooper).idle();
+        for (int i = 0; i < CONSUMING_SAMPLES_PERIOD; ++i) {
+            boolean found = false;
+            while (!found) {
+                for (int j = 0; j < trackFormats.size(); ++j) {
+                    int result = extractor.readSample(j, sampleHolder);
+                    switch (result) {
+                        case SampleSource.SAMPLE_READ:
+                            found = true;
+                            break;
+                        case SampleSource.END_OF_STREAM:
+                            fail("Failed to read samples");
+                            break;
+                        default:
+                    }
+                    if (found) {
+                        break;
+                    }
+                }
+                Shadows.shadowOf(handlerLooper).runOneTask();
+                ShadowLooper.getShadowMainLooper().runOneTask();
+            }
+        }
+        extractor.release();
+    }
+
+    @Ignore("b/70338667")
+    @Test
+    public void testDiskTooSlowTrickplayDisabled() throws Throwable {
+        StorageManager storageManager = new StubStorageManager(RuntimeEnvironment.application);
+        BufferManager bufferManager =
+                new BufferManager(
+                        storageManager, new VerySlowSampleChunk.VerySlowSampleChunkCreator());
+        bufferManager.setMinimumSampleSizeForSpeedCheck(0);
+        MockPlaybackBufferListener listener = new MockPlaybackBufferListener();
+        ExoPlayerSampleExtractor extractor =
+                new ExoPlayerSampleExtractor(
+                        testStreamUri,
+                        dataSource,
+                        bufferManager,
+                        listener,
+                        false,
+                        Looper.getMainLooper(),
+                        handlerThread);
+
+        assertWithMessage("Trickplay should be enabled at the first")
+                .that(Boolean.TRUE)
+                .isEqualTo(listener.getLastState());
+        // Prepares the extractor.
+        extractor.prepare();
+        // Looper is nat available until prepare is called at least once
+        Looper handlerLooper = handlerThread.getLooper();
+        try {
+            while (!extractor.prepare()) {
+
+                ShadowLooper.getShadowMainLooper().runOneTask();
+                Shadows.shadowOf(handlerLooper).runOneTask();
+            }
+        } catch (IOException e) {
+            fail("Exception occurred while preparing: " + e.getMessage());
+        }
+        // Selects all tracks.
+        List<MediaFormat> trackFormats = extractor.getTrackFormats();
+        for (int i = 0; i < trackFormats.size(); ++i) {
+            extractor.selectTrack(i);
+        }
+        // Consumes until once speed check is done.
+        SampleHolder sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
+        sampleHolder.ensureSpaceForWrite(SAMPLE_BUFFER_SIZE);
+        while (!bufferManager.hasSpeedCheckDone()) {
+            boolean found = false;
+            while (!found) {
+                for (int j = 0; j < trackFormats.size(); ++j) {
+                    int result = extractor.readSample(j, sampleHolder);
+                    switch (result) {
+                        case SampleSource.SAMPLE_READ:
+                            found = true;
+                            break;
+                        case SampleSource.END_OF_STREAM:
+                            fail("Failed to read samples");
+                            break;
+                        default:
+                    }
+                    if (found) {
+                        break;
+                    }
+                }
+                ShadowLooper.getShadowMainLooper().runOneTask();
+                Shadows.shadowOf(handlerLooper).runOneTask();
+            }
+        }
+        extractor.release();
+        ShadowLooper.getShadowMainLooper().idle();
+        Shadows.shadowOf(handlerLooper).idle();
+        assertWithMessage("Disk too slow event should be reported")
+                .that(listener.isReportedDiskTooSlow())
+                .isTrue();
+    }
+
+    private static class StubStorageManager implements StorageManager {
+        private final Context mContext;
+
+        StubStorageManager(Context context) {
+            mContext = context;
+        }
+
+        @Override
+        public File getBufferDir() {
+            return mContext.getCacheDir();
+        }
+
+        @Override
+        public boolean isPersistent() {
+            return false;
+        }
+
+        @Override
+        public boolean reachedStorageMax(long bufferSize, long pendingDelete) {
+            return false;
+        }
+
+        @Override
+        public boolean hasEnoughBuffer(long pendingDelete) {
+            return true;
+        }
+
+        @Override
+        public List<BufferManager.TrackFormat> readTrackInfoFiles(boolean isAudio) {
+            return null;
+        }
+
+        @Override
+        public ArrayList<BufferManager.PositionHolder> readIndexFile(String trackId)
+                throws IOException {
+            return null;
+        }
+
+        @Override
+        public void writeTrackInfoFiles(List<BufferManager.TrackFormat> formatList, boolean isAudio)
+                throws IOException {
+            // No-op.
+        }
+
+        @Override
+        public void writeIndexFile(
+                String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index)
+                throws IOException {
+            // No-op.
+        }
+
+        @Override
+        public void updateIndexFile(
+                String trackName, int size, long position, SampleChunk sampleChunk, int offset)
+                throws IOException {
+            // No-op
+        }
+    }
+
+    public static class MockPlaybackBufferListener implements PlaybackBufferListener {
+        private Boolean mLastState;
+        private boolean mIsReportedDiskTooSlow;
+
+        public Boolean getLastState() {
+            return mLastState;
+        }
+
+        public boolean isReportedDiskTooSlow() {
+            return mIsReportedDiskTooSlow;
+        }
+        // PlaybackBufferListener
+        @Override
+        public void onBufferStartTimeChanged(long startTimeMs) {
+            // No-op.
+        }
+
+        @Override
+        public void onBufferStateChanged(boolean available) {
+            mLastState = available;
+        }
+
+        @Override
+        public void onDiskTooSlow() {
+            mIsReportedDiskTooSlow = true;
+        }
+    }
+}
diff --git a/tuner/tests/robotests/javatests/com/android/tv/tuner/tvinput/TunerSessionWorkerExoV2Test.java b/tuner/tests/robotests/javatests/com/android/tv/tuner/tvinput/TunerSessionWorkerExoV2Test.java
new file mode 100644
index 0000000..ad67bb0
--- /dev/null
+++ b/tuner/tests/robotests/javatests/com/android/tv/tuner/tvinput/TunerSessionWorkerExoV2Test.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.tvinput;
+
+import static com.android.tv.common.customization.CustomizationManager.TRICKPLAY_MODE_ENABLED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Application;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.view.accessibility.CaptioningManager;
+
+import com.android.tv.common.CommonConstants;
+import com.android.tv.common.CommonPreferences;
+import com.android.tv.common.compat.TvInputConstantCompat;
+import com.android.tv.common.customization.CustomizationManager;
+import com.android.tv.common.flags.impl.DefaultConcurrentDvrPlaybackFlags;
+import com.android.tv.common.flags.impl.DefaultLegacyFlags;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.android.tv.tuner.exoplayer.MpegTsPlayer;
+import com.android.tv.tuner.source.TsDataSourceManager;
+import com.android.tv.tuner.source.TunerTsStreamerManager;
+import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
+
+import com.google.android.exoplayer.audio.AudioCapabilities;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mockito;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowContextImpl;
+
+import java.lang.reflect.Field;
+
+import javax.inject.Provider;
+
+/** Tests for {@link TunerSessionWorker}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK, application = TestSingletonApp.class)
+public class TunerSessionWorkerExoV2Test {
+
+    private TunerSessionWorkerExoV2 tunerSessionWorker;
+    private int mSignalStrength = TvInputConstantCompat.SIGNAL_STRENGTH_UNKNOWN;
+    private MpegTsPlayer mPlayer = Mockito.mock(MpegTsPlayer.class);
+    private Handler mHandler;
+    private DefaultConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
+    private DefaultLegacyFlags mLegacyFlags;
+
+    @Before
+    public void setUp() throws NoSuchFieldException, IllegalAccessException {
+        Application context = RuntimeEnvironment.application;
+        mConcurrentDvrPlaybackFlags = new DefaultConcurrentDvrPlaybackFlags();
+        mLegacyFlags = DefaultLegacyFlags.DEFAULT;
+        CaptioningManager captioningManager = Mockito.mock(CaptioningManager.class);
+
+        // TODO (b/65160115)
+        Field field = CustomizationManager.class.getDeclaredField("sCustomizationPackage");
+        field.setAccessible(true);
+        field.set(null, CommonConstants.BASE_PACKAGE + ".tuner");
+        field = CustomizationManager.class.getDeclaredField("sTrickplayMode");
+        field.setAccessible(true);
+        field.set(null, TRICKPLAY_MODE_ENABLED);
+
+        ShadowContextImpl shadowContext = Shadow.extract(context.getBaseContext());
+        shadowContext.setSystemService(Context.CAPTIONING_SERVICE, captioningManager);
+
+        CommonPreferences.initialize(context);
+        ChannelDataManager channelDataManager = new ChannelDataManager(context, "testInput");
+
+        mHandler = new Handler(Looper.getMainLooper(), null);
+
+        Provider<TunerTsStreamerManager> tsStreamerManagerProvider =
+                () -> new TunerTsStreamerManager(null);
+        TsDataSourceManager.Factory tsDataSourceManagerFactory =
+                new TsDataSourceManager.Factory(tsStreamerManagerProvider);
+        new TunerSessionExoV2(
+                context,
+                channelDataManager,
+                session -> {},
+                recordingSession -> Uri.parse("recordingUri"),
+                (context1, channelDataManager1, tunerSession1, tunerSessionOverlay) -> {
+                    tunerSessionWorker =
+                            new TunerSessionWorkerExoV2(
+                                    context1,
+                                    channelDataManager1,
+                                    tunerSession1,
+                                    tunerSessionOverlay,
+                                    mHandler,
+                                    mConcurrentDvrPlaybackFlags,
+                                    mLegacyFlags,
+                                    tsDataSourceManagerFactory) {
+                                @Override
+                                protected void notifySignal(int signal) {
+                                    mSignalStrength = signal;
+                                }
+
+                                @Override
+                                protected MpegTsPlayer createPlayer(
+                                        AudioCapabilities capabilities) {
+                                    return mPlayer;
+                                }
+                            };
+                    return tunerSessionWorker;
+                });
+    }
+
+    @Test
+    public void doSelectTrack_mPlayerIsNull() {
+        Message msg = new Message();
+        msg.what = TunerSessionWorker.MSG_SELECT_TRACK;
+        assertThat(tunerSessionWorker.handleMessage(msg)).isFalse();
+    }
+
+    @Test
+    public void doCheckSignalStrength_mPlayerIsNull() {
+        Message msg = new Message();
+        msg.what = TunerSessionWorker.MSG_CHECK_SIGNAL_STRENGTH;
+        assertThat(tunerSessionWorker.handleMessage(msg)).isFalse();
+    }
+
+    @Test
+    public void handleSignal_isNotUsed() {
+        assertThat(tunerSessionWorker.handleSignal(TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED))
+                .isTrue();
+        assertThat(mSignalStrength).isEqualTo(TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED);
+    }
+
+    @Test
+    public void handleSignal_isError() {
+        assertThat(tunerSessionWorker.handleSignal(TvInputConstantCompat.SIGNAL_STRENGTH_ERROR))
+                .isTrue();
+        assertThat(mSignalStrength).isEqualTo(TvInputConstantCompat.SIGNAL_STRENGTH_ERROR);
+    }
+
+    @Test
+    public void handleSignal_isUnknown() {
+        assertThat(tunerSessionWorker.handleSignal(TvInputConstantCompat.SIGNAL_STRENGTH_UNKNOWN))
+                .isTrue();
+        assertThat(mSignalStrength).isEqualTo(TvInputConstantCompat.SIGNAL_STRENGTH_UNKNOWN);
+    }
+
+    @Test
+    public void handleSignal_isNotifySignal() {
+        assertThat(tunerSessionWorker.handleSignal(100)).isTrue();
+        assertThat(mSignalStrength).isEqualTo(100);
+    }
+
+    @Test
+    public void preparePlayback_playerIsNotReady() {
+        Mockito.when(
+                        mPlayer.prepare(
+                                Mockito.eq(RuntimeEnvironment.application),
+                                ArgumentMatchers.any(),
+                                ArgumentMatchers.anyBoolean(),
+                                ArgumentMatchers.any()))
+                .thenReturn(false);
+        tunerSessionWorker.preparePlayback();
+        assertThat(mHandler.hasMessages(TunerSessionWorker.MSG_TUNE)).isFalse();
+        assertThat(mHandler.hasMessages(TunerSessionWorker.MSG_RETRY_PLAYBACK)).isTrue();
+        assertThat(mHandler.hasMessages(TunerSessionWorker.MSG_CHECK_SIGNAL_STRENGTH)).isFalse();
+        assertThat(mHandler.hasMessages(TunerSessionWorker.MSG_CHECK_SIGNAL)).isFalse();
+    }
+
+    @Test
+    @Ignore
+    public void preparePlayback_playerIsReady() {
+        Mockito.when(
+                        mPlayer.prepare(
+                                RuntimeEnvironment.application,
+                                ArgumentMatchers.any(),
+                                ArgumentMatchers.anyBoolean(),
+                                ArgumentMatchers.any()))
+                .thenReturn(true);
+        tunerSessionWorker.preparePlayback();
+        assertThat(mHandler.hasMessages(TunerSessionWorker.MSG_RETRY_PLAYBACK)).isFalse();
+        assertThat(mHandler.hasMessages(TunerSessionWorker.MSG_CHECK_SIGNAL_STRENGTH)).isTrue();
+        assertThat(mHandler.hasMessages(TunerSessionWorker.MSG_CHECK_SIGNAL)).isTrue();
+    }
+}
diff --git a/tuner/tests/robotests/javatests/com/android/tv/tuner/tvinput/TunerSessionWorkerTest.java b/tuner/tests/robotests/javatests/com/android/tv/tuner/tvinput/TunerSessionWorkerTest.java
new file mode 100644
index 0000000..e3fc129
--- /dev/null
+++ b/tuner/tests/robotests/javatests/com/android/tv/tuner/tvinput/TunerSessionWorkerTest.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tv.tuner.tvinput;
+
+import static com.android.tv.common.customization.CustomizationManager.TRICKPLAY_MODE_ENABLED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Application;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.view.accessibility.CaptioningManager;
+
+import com.android.tv.common.CommonConstants;
+import com.android.tv.common.CommonPreferences;
+import com.android.tv.common.compat.TvInputConstantCompat;
+import com.android.tv.common.customization.CustomizationManager;
+import com.android.tv.common.flags.impl.DefaultConcurrentDvrPlaybackFlags;
+import com.android.tv.common.flags.impl.DefaultLegacyFlags;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.android.tv.tuner.exoplayer.MpegTsPlayer;
+import com.android.tv.tuner.source.TsDataSourceManager;
+import com.android.tv.tuner.source.TunerTsStreamerManager;
+import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
+
+import com.google.android.exoplayer.audio.AudioCapabilities;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mockito;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowContextImpl;
+
+import java.lang.reflect.Field;
+
+import javax.inject.Provider;
+
+/** Tests for {@link TunerSessionWorker}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK, application = TestSingletonApp.class)
+public class TunerSessionWorkerTest {
+
+    private TunerSessionWorker tunerSessionWorker;
+    private int mSignalStrength = TvInputConstantCompat.SIGNAL_STRENGTH_UNKNOWN;
+    private MpegTsPlayer mPlayer = Mockito.mock(MpegTsPlayer.class);
+    private Handler mHandler;
+    private DefaultConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags;
+    private DefaultLegacyFlags mLegacyFlags;
+
+    @Before
+    public void setUp() throws NoSuchFieldException, IllegalAccessException {
+        Application context = RuntimeEnvironment.application;
+        CaptioningManager captioningManager = Mockito.mock(CaptioningManager.class);
+        mConcurrentDvrPlaybackFlags = new DefaultConcurrentDvrPlaybackFlags();
+        mLegacyFlags = DefaultLegacyFlags.DEFAULT;
+
+        // TODO (b/65160115)
+        Field field = CustomizationManager.class.getDeclaredField("sCustomizationPackage");
+        field.setAccessible(true);
+        field.set(null, CommonConstants.BASE_PACKAGE + ".tuner");
+        field = CustomizationManager.class.getDeclaredField("sTrickplayMode");
+        field.setAccessible(true);
+        field.set(null, TRICKPLAY_MODE_ENABLED);
+
+        ShadowContextImpl shadowContext = Shadow.extract(context.getBaseContext());
+        shadowContext.setSystemService(Context.CAPTIONING_SERVICE, captioningManager);
+
+        CommonPreferences.initialize(context);
+        ChannelDataManager channelDataManager = new ChannelDataManager(context, "testInput");
+
+        mHandler = new Handler(Looper.getMainLooper(), null);
+        Provider<TunerTsStreamerManager> tsStreamerManagerProvider =
+                () -> new TunerTsStreamerManager(null);
+        TsDataSourceManager.Factory tsdm =
+                new TsDataSourceManager.Factory(tsStreamerManagerProvider);
+
+        new TunerSession(
+                context,
+                channelDataManager,
+                session -> {},
+                recordingSession -> Uri.parse("recordingUri"),
+                (context1, channelDataManager1, tunerSession1, tunerSessionOverlay) -> {
+                    tunerSessionWorker =
+                            new TunerSessionWorker(
+                                    context1,
+                                    channelDataManager1,
+                                    tunerSession1,
+                                    new TunerSessionOverlay(context1),
+                                    mHandler,
+                                    mConcurrentDvrPlaybackFlags,
+                                    mLegacyFlags,
+                                    tsdm) {
+                                @Override
+                                protected void notifySignal(int signal) {
+                                    mSignalStrength = signal;
+                                }
+
+                                @Override
+                                protected MpegTsPlayer createPlayer(
+                                        AudioCapabilities capabilities) {
+                                    return mPlayer;
+                                }
+                            };
+                    return tunerSessionWorker;
+                });
+    }
+
+    @Test
+    public void doSelectTrack_mPlayerIsNull() {
+        Message msg = new Message();
+        msg.what = TunerSessionWorker.MSG_SELECT_TRACK;
+        assertThat(tunerSessionWorker.handleMessage(msg)).isFalse();
+    }
+
+    @Test
+    public void doCheckSignalStrength_mPlayerIsNull() {
+        Message msg = new Message();
+        msg.what = TunerSessionWorker.MSG_CHECK_SIGNAL_STRENGTH;
+        assertThat(tunerSessionWorker.handleMessage(msg)).isFalse();
+    }
+
+    @Test
+    public void handleSignal_isNotUsed() {
+        assertThat(tunerSessionWorker.handleSignal(TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED))
+                .isTrue();
+        assertThat(mSignalStrength).isEqualTo(TvInputConstantCompat.SIGNAL_STRENGTH_NOT_USED);
+    }
+
+    @Test
+    public void handleSignal_isError() {
+        assertThat(tunerSessionWorker.handleSignal(TvInputConstantCompat.SIGNAL_STRENGTH_ERROR))
+                .isTrue();
+        assertThat(mSignalStrength).isEqualTo(TvInputConstantCompat.SIGNAL_STRENGTH_ERROR);
+    }
+
+    @Test
+    public void handleSignal_isUnknown() {
+        assertThat(tunerSessionWorker.handleSignal(TvInputConstantCompat.SIGNAL_STRENGTH_UNKNOWN))
+                .isTrue();
+        assertThat(mSignalStrength).isEqualTo(TvInputConstantCompat.SIGNAL_STRENGTH_UNKNOWN);
+    }
+
+    @Test
+    public void handleSignal_isNotifySignal() {
+        assertThat(tunerSessionWorker.handleSignal(100)).isTrue();
+        assertThat(mSignalStrength).isEqualTo(100);
+    }
+
+    @Test
+    public void preparePlayback_playerIsNotReady() {
+        Mockito.when(
+                        mPlayer.prepare(
+                                Mockito.eq(RuntimeEnvironment.application),
+                                ArgumentMatchers.any(),
+                                ArgumentMatchers.anyBoolean(),
+                                ArgumentMatchers.any()))
+                .thenReturn(false);
+        tunerSessionWorker.preparePlayback();
+        assertThat(mHandler.hasMessages(TunerSessionWorker.MSG_TUNE)).isFalse();
+        assertThat(mHandler.hasMessages(TunerSessionWorker.MSG_RETRY_PLAYBACK)).isTrue();
+        assertThat(mHandler.hasMessages(TunerSessionWorker.MSG_CHECK_SIGNAL_STRENGTH)).isFalse();
+        assertThat(mHandler.hasMessages(TunerSessionWorker.MSG_CHECK_SIGNAL)).isFalse();
+    }
+
+    @Test
+    @Ignore
+    public void preparePlayback_playerIsReady() {
+        Mockito.when(
+                        mPlayer.prepare(
+                                RuntimeEnvironment.application,
+                                ArgumentMatchers.any(),
+                                ArgumentMatchers.anyBoolean(),
+                                ArgumentMatchers.any()))
+                .thenReturn(true);
+        tunerSessionWorker.preparePlayback();
+        assertThat(mHandler.hasMessages(TunerSessionWorker.MSG_RETRY_PLAYBACK)).isFalse();
+        assertThat(mHandler.hasMessages(TunerSessionWorker.MSG_CHECK_SIGNAL_STRENGTH)).isTrue();
+        assertThat(mHandler.hasMessages(TunerSessionWorker.MSG_CHECK_SIGNAL)).isTrue();
+    }
+}
diff --git a/tuner/tests/robotests/javatests/com/android/tv/tuner/tvinput/datamanager/ChannelDataManagerTest.java b/tuner/tests/robotests/javatests/com/android/tv/tuner/tvinput/datamanager/ChannelDataManagerTest.java
new file mode 100644
index 0000000..12f4d23
--- /dev/null
+++ b/tuner/tests/robotests/javatests/com/android/tv/tuner/tvinput/datamanager/ChannelDataManagerTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.tvinput.datamanager;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ContentValues;
+import android.content.pm.ProviderInfo;
+import android.media.tv.TvContract;
+import com.android.tv.testing.TestSingletonApp;
+import com.android.tv.testing.constants.ConfigConstants;
+import com.android.tv.testing.fakes.FakeTvProvider;
+import com.android.tv.tuner.data.Channel;
+import com.android.tv.tuner.data.TunerChannel;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowContentResolver;
+import org.robolectric.shadows.ShadowContextWrapper;
+
+/** Tests for {@link com.android.tv.tuner.tvinput.datamanager.ChannelDataManager}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK, application = TestSingletonApp.class)
+public class ChannelDataManagerTest {
+
+    private ChannelDataManager mChannelDataManager;
+
+    @Before
+    public void setup() {
+        ProviderInfo info = new ProviderInfo();
+        info.authority = TvContract.AUTHORITY;
+        FakeTvProvider provider =
+                Robolectric.buildContentProvider(FakeTvProvider.class).create(info).get();
+        provider.setCallingPackage("com.android.tv");
+        provider.onCreate();
+        ShadowContextWrapper shadowContextWrapper = new ShadowContextWrapper();
+        shadowContextWrapper.grantPermissions(android.Manifest.permission.MODIFY_PARENTAL_CONTROLS);
+        ShadowContentResolver.registerProviderInternal(TvContract.AUTHORITY, provider);
+        provider.delete(TvContract.Channels.CONTENT_URI, null, null);
+        ContentValues contentValues = new ContentValues();
+        contentValues.put(TvContract.Channels.COLUMN_INPUT_ID, "com.android.tv");
+        contentValues.put(
+                TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA,
+                Channel.TunerChannelProto.getDefaultInstance().toByteArray());
+        contentValues.put(TvContract.Channels.COLUMN_LOCKED, 0);
+        provider.insert(TvContract.Channels.CONTENT_URI, contentValues);
+        contentValues.put(TvContract.Channels.COLUMN_LOCKED, 1);
+        provider.insert(TvContract.Channels.CONTENT_URI, contentValues);
+
+        mChannelDataManager = new ChannelDataManager(RuntimeEnvironment.application, "testInput");
+    }
+
+    @After
+    public void tearDown() {
+        mChannelDataManager.releaseSafely();
+    }
+
+    @Test
+    public void getChannel_locked() {
+        TunerChannel tunerChannel = mChannelDataManager.getChannel(2L);
+        assertThat(tunerChannel.isLocked()).isTrue();
+    }
+
+    @Test
+    public void getChannel_unlocked() {
+        TunerChannel tunerChannel = mChannelDataManager.getChannel(1L);
+        assertThat(tunerChannel.isLocked()).isFalse();
+    }
+}
diff --git a/tuner/tests/robotests/javatests/com/android/tv/tuner/util/PostalCodeUtilsTest.java b/tuner/tests/robotests/javatests/com/android/tv/tuner/util/PostalCodeUtilsTest.java
new file mode 100644
index 0000000..36128c1
--- /dev/null
+++ b/tuner/tests/robotests/javatests/com/android/tv/tuner/util/PostalCodeUtilsTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.tv.common.util.PostalCodeUtils;
+import com.android.tv.testing.constants.ConfigConstants;
+import java.util.Locale;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link PostalCodeUtils} */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = ConfigConstants.SDK)
+public class PostalCodeUtilsTest {
+
+    private static final String[] VALID_POSTCODES_US = {"94043", "94063", "90007"};
+    private static final String[] INVALID_POSTCODES_US = {
+        "", "9404", "ABC", "BCD8", "G777", "BT777", "OXX33", "E1WW", "SW1XX", "E11W", "SW10X",
+        "GIR", "B8", "G77", "BT7", "OX33", "E1W", "SW1X"
+    };
+    private static final String[] VALID_POSTCODES_GB = {
+        "GIR", "B8", "G77", "BT7", "OX33", "E1W", "SW1X", "GIR 0AA", "GIR0AA", "B8 2NE", "PR10BJ"
+    };
+    private static final String[] INVALID_POSTCODES_GB = {
+        "", "9404", "ABC", "BCD8", "G777", "BT777", "OXX33", "E1WW", "SW1XX", "E11W", "SW10X",
+        "94043", "94063", "90007", "B8 ", "OX331D"
+    };
+
+    @Test
+    public void validPostcodesUs() {
+        for (String postcode : VALID_POSTCODES_US) {
+            assertThat(PostalCodeUtils.matches(postcode, Locale.US.getCountry())).isTrue();
+        }
+    }
+
+    @Test
+    public void validPostcodesGb() {
+        for (String postcode : VALID_POSTCODES_GB) {
+            assertThat(PostalCodeUtils.matches(postcode, Locale.UK.getCountry())).isTrue();
+        }
+    }
+
+    @Test
+    public void invalidPostcodesUs() {
+        for (String postcode : INVALID_POSTCODES_US) {
+            assertThat(PostalCodeUtils.matches(postcode, Locale.US.getCountry())).isFalse();
+        }
+    }
+
+    @Test
+    public void invalidPostcodesGb() {
+        for (String postcode : INVALID_POSTCODES_GB) {
+            assertThat(PostalCodeUtils.matches(postcode, Locale.UK.getCountry())).isFalse();
+        }
+    }
+
+    @Test
+    public void unsupportedRegion() {
+        for (String postcode : INVALID_POSTCODES_US) {
+            // {@link Locale.ROOT} is an empty Locale
+            assertThat(PostalCodeUtils.matches(postcode, Locale.ROOT.getCountry())).isTrue();
+        }
+    }
+}
diff --git a/tuner/tests/testing/AndroidManifest.xml b/tuner/tests/testing/AndroidManifest.xml
index 7e07a52..9fcecf9 100644
--- a/tuner/tests/testing/AndroidManifest.xml
+++ b/tuner/tests/testing/AndroidManifest.xml
@@ -18,6 +18,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.tv.tuner.testing"
           android:versionCode="1">
-  <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
+  <uses-sdk android:targetSdkVersion="28" android:minSdkVersion="23"/>
     <application />
 </manifest>
diff --git a/tuner/tests/unittests/javatests/AndroidManifest.xml b/tuner/tests/unittests/javatests/AndroidManifest.xml
index 62caefa..ddbddd0 100644
--- a/tuner/tests/unittests/javatests/AndroidManifest.xml
+++ b/tuner/tests/unittests/javatests/AndroidManifest.xml
@@ -18,7 +18,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.tv.tuner.layout.tests" >
 
-    <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="27"/>
+    <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="28"/>
 
     <instrumentation
         android:name="android.test.InstrumentationTestRunner"
diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/AndroidManifest.xml b/tuner/tests/unittests/javatests/com/android/tv/tuner/AndroidManifest.xml
index 6fe0b85..6956426 100644
--- a/tuner/tests/unittests/javatests/com/android/tv/tuner/AndroidManifest.xml
+++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/AndroidManifest.xml
@@ -18,11 +18,11 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.tv.tuner.tests" >
 
-    <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="27" />
+    <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="28" />
 
     <instrumentation
         android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.tv" />
+        android:targetPackage="com.android.tv.tuner.sample.dvb" />
 
     <application android:label="TunerTvInputTests" >
         <uses-library android:name="android.test.runner" />
diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/ZappingTimeTest.java b/tuner/tests/unittests/javatests/com/android/tv/tuner/ZappingTimeTest.java
index ef653f8..fb9c635 100644
--- a/tuner/tests/unittests/javatests/com/android/tv/tuner/ZappingTimeTest.java
+++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/ZappingTimeTest.java
@@ -24,21 +24,29 @@
 import android.test.InstrumentationTestCase;
 import android.util.Log;
 import android.view.Surface;
+
 import androidx.test.filters.LargeTest;
+
 import com.android.tv.common.flags.impl.DefaultConcurrentDvrPlaybackFlags;
 import com.android.tv.tuner.data.Cea708Data;
+import com.android.tv.tuner.data.Channel.AudioStreamType;
+import com.android.tv.tuner.data.Channel.VideoStreamType;
 import com.android.tv.tuner.data.PsiData;
 import com.android.tv.tuner.data.PsipData;
 import com.android.tv.tuner.data.TunerChannel;
-import com.android.tv.tuner.data.nano.Channel;
 import com.android.tv.tuner.exoplayer.MpegTsPlayer;
 import com.android.tv.tuner.exoplayer.MpegTsRendererBuilder;
 import com.android.tv.tuner.exoplayer.buffer.BufferManager;
 import com.android.tv.tuner.exoplayer.buffer.PlaybackBufferListener;
 import com.android.tv.tuner.exoplayer.buffer.TrickplayStorageManager;
 import com.android.tv.tuner.source.TsDataSourceManager;
+import com.android.tv.tuner.source.TsDataSourceManager.Factory;
 import com.android.tv.tuner.ts.EventDetector.EventListener;
+
 import com.google.android.exoplayer.ExoPlayer;
+
+import org.junit.Ignore;
+
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -49,7 +57,6 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
-import org.junit.Ignore;
 
 /** This class use {@link FileTunerHal} to simulate tunerhal's actions to test zapping time. */
 @LargeTest
@@ -99,10 +106,10 @@
         HandlerThread handlerThread = new HandlerThread(TAG);
         handlerThread.start();
         List<PsiData.PmtItem> pmtItems = new ArrayList<>();
-        pmtItems.add(new PsiData.PmtItem(Channel.VideoStreamType.MPEG2, VIDEO_PID, null, null));
+        pmtItems.add(new PsiData.PmtItem(VideoStreamType.MPEG2_VALUE, VIDEO_PID, null, null));
         for (int audioPid : AUDIO_PIDS) {
             pmtItems.add(
-                    new PsiData.PmtItem(Channel.AudioStreamType.A52AC3AUDIO, audioPid, null, null));
+                    new PsiData.PmtItem(AudioStreamType.A52AC3AUDIO_VALUE, audioPid, null, null));
         }
 
         Context context = getInstrumentation().getContext();
@@ -117,7 +124,8 @@
         mChannel.setModulation(MODULATION);
         mTunerHal = new FileTunerHal(context, tsCacheFile);
         mTunerHal.openFirstAvailable();
-        mSourceManager = TsDataSourceManager.createSourceManager(false);
+        TsDataSourceManager.Factory tsFactory = new Factory(null);
+        mSourceManager = tsFactory.create(false);
         mSourceManager.addTunerHalForTest(mTunerHal);
         mHandler =
                 new Handler(
@@ -155,8 +163,7 @@
                                                             new MpegTsRendererBuilder(
                                                                     mTargetContext,
                                                                     bufferManager,
-                                                                    mPlaybackBufferListener,
-                                                                    mConcurrentDvrPlaybackFlags),
+                                                                    mPlaybackBufferListener),
                                                             mHandler,
                                                             mSourceManager,
                                                             null,
diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/AndroidManifest.xml b/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/AndroidManifest.xml
index 77c7f40..79b0987 100644
--- a/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/AndroidManifest.xml
+++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/layout/tests/AndroidManifest.xml
@@ -17,7 +17,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.tv.tuner"
     android:versionCode="1">
-  <uses-sdk android:targetSdkVersion="27" android:minSdkVersion="23"/>
+  <uses-sdk android:targetSdkVersion="28" android:minSdkVersion="23"/>
   <application android:label="TunerTvInputLayoutTests" >
     <activity android:name="com.android.tv.tuner.layout.tests.ScaledLayoutActivity"
               android:label="ScaledLayout Test" />
diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/setup/AndroidManifest.xml b/tuner/tests/unittests/javatests/com/android/tv/tuner/setup/AndroidManifest.xml
new file mode 100644
index 0000000..19cc0e5
--- /dev/null
+++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/setup/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.tv.tuner.setup.tests" >
+
+    <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="28" />
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.tv.tuner.sample.dvb" />
+
+    <application android:label="TunerTvInputTests" >
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+</manifest>
diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/setup/TunerHalCreatorTest.java b/tuner/tests/unittests/javatests/com/android/tv/tuner/setup/TunerHalCreatorTest.java
index a3a3208..cc5e5c5 100644
--- a/tuner/tests/unittests/javatests/com/android/tv/tuner/setup/TunerHalCreatorTest.java
+++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/setup/TunerHalCreatorTest.java
@@ -21,14 +21,18 @@
 import static org.junit.Assert.assertSame;
 
 import android.os.AsyncTask;
+
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
+
 import com.android.tv.tuner.api.Tuner;
 import com.android.tv.tuner.setup.BaseTunerSetupActivity.TunerHalCreator;
-import java.util.concurrent.Executor;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.concurrent.Executor;
+
 /** Tests for {@link TunerHalCreator}. */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -37,7 +41,7 @@
 
     private static class TestTunerHalCreator extends TunerHalCreator {
         private TestTunerHalCreator(Executor executor) {
-            super(null, executor);
+            super(null, executor, null);
         }
 
         @Override