Merge Android Pie into master

Bug: 112104996
Change-Id: Iff7d4076435e20b6283291a6fc770fbe85971d71
diff --git a/Android.mk b/Android.mk
index e157d6b..5195a10 100644
--- a/Android.mk
+++ b/Android.mk
@@ -16,14 +16,15 @@
 
 ifneq ($(TARGET_BUILD_PDK), true)
 
-LOCAL_PATH:= $(call my-dir)
+LOCAL_PATH := $(call my-dir)
+CAR_BROADCASTRADIO_SUPPORTLIB_PATH := packages/apps/Car/libs/car-broadcastradio-support
 
 include $(CLEAR_VARS)
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src) $(call all-Iaidl-files-under, src)
-LOCAL_AIDL_INCLUDES := $(call all-Iaidl-files-under, src)
-
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_AIDL_INCLUDES := \
+    $(LOCAL_PATH)/src \
+    $(CAR_BROADCASTRADIO_SUPPORTLIB_PATH)/src
 
 LOCAL_PACKAGE_NAME := CarRadioApp
 LOCAL_PRIVATE_PLATFORM_APIS := true
@@ -36,29 +37,73 @@
 
 LOCAL_USE_AAPT2 := true
 
-LOCAL_STATIC_ANDROID_LIBRARIES += \
-    android-support-v4 \
-    android-support-design \
-    car-radio-service
+LOCAL_JAVA_LIBRARIES += android.car
 
-LOCAL_STATIC_JAVA_LIBRARIES += \
-    car-stream-lib
+LOCAL_STATIC_ANDROID_LIBRARIES += \
+    android-support-car \
+    android-support-constraint-layout \
+    car-apps-common \
+    car-broadcastradio-support \
+    car-stream-ui-lib
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    android-arch-lifecycle-livedata \
+    android-arch-persistence-db-framework \
+    android-arch-persistence-db \
+    android-support-constraint-layout-solver \
+    bcradio-android-arch-room-common-nodeps \
+    bcradio-android-arch-room-runtime-nodeps
+
+LOCAL_ANNOTATION_PROCESSORS := \
+    bcradio-android-arch-room-common-nodeps \
+    bcradio-android-arch-room-compiler-nodeps \
+    bcradio-android-arch-room-migration-nodeps \
+    bcradio-android-support-annotations-nodeps \
+    bcradio-antlr4-nodeps \
+    bcradio-apache-commons-codec-nodeps \
+    bcradio-auto-common-nodeps \
+    bcradio-javapoet-nodeps \
+    bcradio-kotlin-metadata-nodeps \
+    bcradio-sqlite-jdbc-nodeps \
+    guava-21.0 \
+    kotlin-stdlib
+
+LOCAL_ANNOTATION_PROCESSOR_CLASSES := \
+    android.arch.persistence.room.RoomProcessor
 
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
 
-# Include support-v7-appcompat, if not already included
-ifeq (,$(findstring android-support-v7-appcompat,$(LOCAL_STATIC_ANDROID_LIBRARIES)))
-LOCAL_STATIC_ANDROID_LIBRARIES += android-support-v7-appcompat
-endif
-
 LOCAL_PROGUARD_ENABLED := disabled
 
 LOCAL_DEX_PREOPT := false
 
-include packages/apps/Car/libs/car-stream-ui-lib/car-stream-ui-lib.mk
-include packages/apps/Car/libs/car-apps-common/car-apps-common.mk
-include packages/services/Car/car-support-lib/car-support.mk
-
 include $(BUILD_PACKAGE)
 
+include $(CLEAR_VARS)
+
+LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES := \
+    bcradio-android-arch-room-runtime-nodeps:libs/android-arch/room/runtime-1.1.0-beta3.aar \
+    bcradio-android-arch-room-common-nodeps:libs/android-arch/room/common-1.1.0-beta3.jar
+
+include $(BUILD_MULTI_PREBUILT)
+
+include $(CLEAR_VARS)
+
+COMMON_LIBS_PATH := ../../../../prebuilts/tools/common/m2/repository
+MAVEN_LIBS_PATH := ../../../../prebuilts/maven_repo/android
+
+LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES := \
+    bcradio-android-arch-room-common-nodeps:libs/android-arch/room/common-1.1.0-beta3.jar \
+    bcradio-android-arch-room-compiler-nodeps:libs/android-arch/room/compiler-1.1.0-beta3.jar \
+    bcradio-android-arch-room-migration-nodeps:libs/android-arch/room/migration-1.1.0-beta3.jar \
+    bcradio-android-support-annotations-nodeps:$(MAVEN_LIBS_PATH)/com/android/support/support-annotations/27.1.0/support-annotations-27.1.0.jar \
+    bcradio-antlr4-nodeps:$(COMMON_LIBS_PATH)/org/antlr/antlr4/4.5.3/antlr4-4.5.3.jar \
+    bcradio-apache-commons-codec-nodeps:$(COMMON_LIBS_PATH)/org/eclipse/tycho/tycho-bundles-external/0.18.1/eclipse/plugins/org.apache.commons.codec_1.4.0.v201209201156.jar \
+    bcradio-auto-common-nodeps:$(COMMON_LIBS_PATH)/com/google/auto/auto-common/0.9/auto-common-0.9.jar \
+    bcradio-javapoet-nodeps:$(COMMON_LIBS_PATH)/com/squareup/javapoet/1.8.0/javapoet-1.8.0.jar \
+    bcradio-kotlin-metadata-nodeps:$(COMMON_LIBS_PATH)/me/eugeniomarletti/kotlin-metadata/1.2.1/kotlin-metadata-1.2.1.jar \
+    bcradio-sqlite-jdbc-nodeps:$(COMMON_LIBS_PATH)/org/xerial/sqlite-jdbc/3.20.1/sqlite-jdbc-3.20.1.jar
+
+include $(BUILD_HOST_PREBUILT)
+
 endif
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index fea3fbf..b4a45bc 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -22,10 +22,10 @@
         android:targetSdkVersion="25" />
 
     <!-- This permission is required to allow the radio to be muted. -->
+    <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_SETTINGS" />
     <uses-permission android:name="android.car.permission.CAR_CONTROL_AUDIO_VOLUME" />
     <uses-permission android:name="android.car.permission.CAR_RADIO" />
     <uses-permission android:name="android.permission.ACCESS_BROADCAST_RADIO" />
-    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
 
     <application android:label="@string/app_name"
         android:icon="@drawable/logo_fm_radio"
@@ -34,7 +34,6 @@
         <activity android:name=".CarRadioActivity"
             android:theme="@style/CarRadioAppTheme"
             android:launchMode="singleTask"
-            android:label="@string/app_name"
             android:resizeableActivity="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -45,14 +44,11 @@
 
         <service
             android:name=".RadioService"
-            android:label="@string/service_name"
             android:exported="true">
-        </service>
-
-        <receiver android:name=".BootupReceiver">
             <intent-filter>
-                <action android:name="android.intent.action.BOOT_COMPLETED" />
+                <action android:name="android.media.browse.MediaBrowserService" />
+                <action android:name="android.car.intent.action.PLAY_BROADCASTRADIO" />
             </intent-filter>
-        </receiver>
+        </service>
     </application>
 </manifest>
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
new file mode 100644
index 0000000..38f9800
--- /dev/null
+++ b/PREUPLOAD.cfg
@@ -0,0 +1,7 @@
+[Hook Scripts]
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
+ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py -f ${PREUPLOAD_FILES}
+
+[Builtin Hooks]
+commit_msg_changeid_field = true
+commit_msg_test_field = true
diff --git a/libs/android-arch/room/common-1.1.0-beta3.jar b/libs/android-arch/room/common-1.1.0-beta3.jar
new file mode 100644
index 0000000..8539f0b
--- /dev/null
+++ b/libs/android-arch/room/common-1.1.0-beta3.jar
Binary files differ
diff --git a/libs/android-arch/room/compiler-1.1.0-beta3.jar b/libs/android-arch/room/compiler-1.1.0-beta3.jar
new file mode 100644
index 0000000..9fe428f
--- /dev/null
+++ b/libs/android-arch/room/compiler-1.1.0-beta3.jar
Binary files differ
diff --git a/libs/android-arch/room/migration-1.1.0-beta3.jar b/libs/android-arch/room/migration-1.1.0-beta3.jar
new file mode 100644
index 0000000..61f4559
--- /dev/null
+++ b/libs/android-arch/room/migration-1.1.0-beta3.jar
Binary files differ
diff --git a/libs/android-arch/room/runtime-1.1.0-beta3.aar b/libs/android-arch/room/runtime-1.1.0-beta3.aar
new file mode 100644
index 0000000..89093f7
--- /dev/null
+++ b/libs/android-arch/room/runtime-1.1.0-beta3.aar
Binary files differ
diff --git a/res/anim/preset_list_slide_up.xml b/res/anim/preset_list_slide_up.xml
deleted file mode 100644
index 13ca416..0000000
--- a/res/anim/preset_list_slide_up.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-     android:interpolator="@android:interpolator/fast_out_slow_in">
-
-    <translate
-        android:duration="500"
-        android:startOffset="@integer/radio_preset_list_slide_up_delay"
-        android:fromYDelta="100%"
-        android:toYDelta="0" />
-</set>
diff --git a/res/drawable-hdpi/logo_fm_radio.png b/res/drawable-hdpi/logo_fm_radio.png
index b618655..a950e41 100644
--- a/res/drawable-hdpi/logo_fm_radio.png
+++ b/res/drawable-hdpi/logo_fm_radio.png
Binary files differ
diff --git a/res/drawable-mdpi/logo_fm_radio.png b/res/drawable-mdpi/logo_fm_radio.png
index 36680e9..aa637e5 100644
--- a/res/drawable-mdpi/logo_fm_radio.png
+++ b/res/drawable-mdpi/logo_fm_radio.png
Binary files differ
diff --git a/res/drawable-xhdpi/logo_fm_radio.png b/res/drawable-xhdpi/logo_fm_radio.png
index e46b385..f05ac95 100644
--- a/res/drawable-xhdpi/logo_fm_radio.png
+++ b/res/drawable-xhdpi/logo_fm_radio.png
Binary files differ
diff --git a/res/drawable-xxhdpi/logo_fm_radio.png b/res/drawable-xxhdpi/logo_fm_radio.png
index 72267d0..ebcefbe 100644
--- a/res/drawable-xxhdpi/logo_fm_radio.png
+++ b/res/drawable-xxhdpi/logo_fm_radio.png
Binary files differ
diff --git a/res/drawable/ic_arrow_drop_down.xml b/res/drawable/ic_arrow_drop_down.xml
deleted file mode 100644
index 1de2dc4..0000000
--- a/res/drawable/ic_arrow_drop_down.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="36dp"
-        android:height="36dp"
-        android:viewportWidth="36.0"
-        android:viewportHeight="36.0">
-    <path
-        android:pathData="M10.5,15l7.5,7.5 7.5,-7.5z"
-        android:fillColor="@color/car_radio_control_button"/>
-</vector>
diff --git a/res/drawable/ic_equalizer.xml b/res/drawable/ic_equalizer.xml
index db47773..e9265ce 100644
--- a/res/drawable/ic_equalizer.xml
+++ b/res/drawable/ic_equalizer.xml
@@ -20,5 +20,5 @@
         android:viewportHeight="48.0">
     <path
         android:pathData="M20,40h8L28,8h-8v32zM8,40h8L16,24L8,24v16zM32,18v22h8L40,18h-8z"
-        android:fillColor="@color/car_radio_control_button"/>
+        android:fillColor="@color/car_radio_equalizer"/>
 </vector>
diff --git a/res/drawable/ic_pause.xml b/res/drawable/ic_pause.xml
index 4625866..6728b9d 100644
--- a/res/drawable/ic_pause.xml
+++ b/res/drawable/ic_pause.xml
@@ -14,8 +14,8 @@
 limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/stream_button_icon_size"
-    android:height="@dimen/stream_button_icon_size"
+    android:width="@dimen/car_primary_icon_size"
+    android:height="@dimen/car_primary_icon_size"
     android:viewportWidth="48"
     android:viewportHeight="48">
 
diff --git a/res/drawable/ic_play_arrow.xml b/res/drawable/ic_play_arrow.xml
index 6903b96..6b96bf7 100644
--- a/res/drawable/ic_play_arrow.xml
+++ b/res/drawable/ic_play_arrow.xml
@@ -14,8 +14,8 @@
 limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/stream_button_icon_size"
-    android:height="@dimen/stream_button_icon_size"
+    android:width="@dimen/car_primary_icon_size"
+    android:height="@dimen/car_primary_icon_size"
     android:viewportWidth="48"
     android:viewportHeight="48">
 
diff --git a/res/drawable/ic_presets_list.xml b/res/drawable/ic_presets_list.xml
index 105492f..f7e5b5c 100644
--- a/res/drawable/ic_presets_list.xml
+++ b/res/drawable/ic_presets_list.xml
@@ -14,8 +14,8 @@
 limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="@dimen/stream_button_icon_size"
-        android:height="@dimen/stream_button_icon_size"
+        android:width="@dimen/car_primary_icon_size"
+        android:height="@dimen/car_primary_icon_size"
         android:viewportWidth="24.0"
         android:viewportHeight="24.0">
     <path
diff --git a/res/drawable/ic_skip_next.xml b/res/drawable/ic_skip_next.xml
index 6151c21..5f9d655 100644
--- a/res/drawable/ic_skip_next.xml
+++ b/res/drawable/ic_skip_next.xml
@@ -14,8 +14,8 @@
 limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/stream_button_icon_size"
-    android:height="@dimen/stream_button_icon_size"
+    android:width="@dimen/car_primary_icon_size"
+    android:height="@dimen/car_primary_icon_size"
     android:viewportWidth="48"
     android:viewportHeight="48">
 
diff --git a/res/drawable/ic_skip_previous.xml b/res/drawable/ic_skip_previous.xml
index 88ff7d2..c3eef5e 100644
--- a/res/drawable/ic_skip_previous.xml
+++ b/res/drawable/ic_skip_previous.xml
@@ -14,8 +14,8 @@
 limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/stream_button_icon_size"
-    android:height="@dimen/stream_button_icon_size"
+    android:width="@dimen/car_primary_icon_size"
+    android:height="@dimen/car_primary_icon_size"
     android:viewportWidth="48"
     android:viewportHeight="48">
 
diff --git a/res/drawable/ic_star_empty.xml b/res/drawable/ic_star_empty.xml
index d8c1044..83b0d67 100644
--- a/res/drawable/ic_star_empty.xml
+++ b/res/drawable/ic_star_empty.xml
@@ -14,8 +14,8 @@
 limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="@dimen/stream_button_icon_size"
-        android:height="@dimen/stream_button_icon_size"
+        android:width="@dimen/car_primary_icon_size"
+        android:height="@dimen/car_primary_icon_size"
         android:viewportWidth="24.0"
         android:viewportHeight="24.0">
     <path
diff --git a/res/drawable/ic_star_filled.xml b/res/drawable/ic_star_filled.xml
index a14445c..64f2ceb 100644
--- a/res/drawable/ic_star_filled.xml
+++ b/res/drawable/ic_star_filled.xml
@@ -14,8 +14,8 @@
 limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="@dimen/stream_button_icon_size"
-        android:height="@dimen/stream_button_icon_size"
+        android:width="@dimen/car_primary_icon_size"
+        android:height="@dimen/car_primary_icon_size"
         android:viewportWidth="24.0"
         android:viewportHeight="24.0">
     <path
diff --git a/res/drawable/preset_item_card_rounded_bg.xml b/res/drawable/preset_item_card_rounded_bg.xml
deleted file mode 100644
index ffe1625..0000000
--- a/res/drawable/preset_item_card_rounded_bg.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<shape xmlns:android="http://schemas.android.com/apk/res/android">
-    <solid android:color="@color/car_card" />
-    <corners
-        android:radius="@dimen/car_preset_card_radius"/>
-</shape>
diff --git a/res/drawable/preset_item_card_rounded_bottom_bg.xml b/res/drawable/preset_item_card_rounded_bottom_bg.xml
deleted file mode 100644
index 1e8a915..0000000
--- a/res/drawable/preset_item_card_rounded_bottom_bg.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<shape xmlns:android="http://schemas.android.com/apk/res/android">
-    <solid android:color="@color/car_card" />
-    <corners
-        android:bottomRightRadius="@dimen/car_preset_card_radius"
-        android:bottomLeftRadius="@dimen/car_preset_card_radius"/>
-</shape>
diff --git a/res/drawable/preset_item_card_rounded_top_bg.xml b/res/drawable/preset_item_card_rounded_top_bg.xml
deleted file mode 100644
index 392d1d4..0000000
--- a/res/drawable/preset_item_card_rounded_top_bg.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<shape xmlns:android="http://schemas.android.com/apk/res/android">
-    <solid android:color="@color/car_card" />
-    <corners
-        android:topRightRadius="@dimen/car_preset_card_radius"
-        android:topLeftRadius="@dimen/car_preset_card_radius" />
-</shape>
diff --git a/res/drawable/radio_control_background.xml b/res/drawable/radio_control_background.xml
index cfcb215..a883403 100644
--- a/res/drawable/radio_control_background.xml
+++ b/res/drawable/radio_control_background.xml
@@ -14,5 +14,5 @@
 limitations under the License.
 -->
 <ripple xmlns:android="http://schemas.android.com/apk/res/android"
-        android:color="@color/radio_ripple_background">
+        android:color="@color/car_card_ripple_background">
 </ripple>
diff --git a/res/layout/manual_tuner.xml b/res/layout/manual_tuner.xml
index 0586920..c74f201 100644
--- a/res/layout/manual_tuner.xml
+++ b/res/layout/manual_tuner.xml
@@ -13,119 +13,87 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<FrameLayout
+<!-- This Layout is clickable so that clicks do not fall through to the underlying
+     fragment. -->
+<android.support.constraint.ConstraintLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_marginTop="@dimen/car_app_bar_height"
+    android:background="@color/car_card"
+    android:clickable="true"
     android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:paddingTop="@dimen/lens_header_height" >
+    android:layout_height="match_parent">
 
-    <!-- This LinearLayout is clickable so that clicks do not fall through to the underlying
-         fragment. -->
-    <LinearLayout
-        android:background="@color/car_card"
-        android:clickable="true"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:paddingTop="@dimen/manual_tuner_top_padding">
+    <android.support.constraint.Guideline
+        android:id="@+id/center_guideline"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintGuide_percent="0.55"/>
+    <ImageView
+        android:id="@+id/exit_manual_tuner_button"
+        android:background="@drawable/radio_control_background"
+        android:layout_marginTop="@dimen/radio_default_spacing"
+        android:layout_marginStart="@dimen/radio_default_spacing"
+        android:layout_width="@dimen/car_touch_target_size"
+        android:layout_height="@dimen/car_touch_target_size"
+        android:scaleType="center"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        android:src="@drawable/ic_down_outlined" />
 
-        <FrameLayout
-            android:layout_marginTop="@dimen/manual_tuner_dialpad_top_margin"
-            android:layout_width="0dp"
-            android:layout_height="match_parent"
-            android:layout_weight="1" >
+    <include
+        android:id="@+id/dialpad_layout"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toStartOf="@+id/center_guideline"
+        android:layout_width="0dp"
+        android:layout_height="@dimen/manual_tuner_container_height"
+        layout="@layout/manual_tuner_dialpad" />
 
-            <!-- This FrameLayout ensures that the back button is centered within the
-                 manual_tuner_back_button_container_size despite the button's touch target
-                 being smaller. -->
-            <FrameLayout
-                android:layout_marginTop="@dimen/manual_tuner_back_button_top_margin"
-                android:layout_gravity="start|top"
-                android:layout_width="@dimen/manual_tuner_back_button_container_size"
-                android:layout_height="@dimen/manual_tuner_back_button_container_size">
+    <com.android.car.radio.RadioBandButton
+        android:id="@+id/manual_tuner_am_band"
+        android:background="@drawable/radio_control_background"
+        android:layout_marginEnd="@dimen/radio_default_spacing"
+        app:layout_constraintEnd_toStartOf="@+id/manual_tuner_fm_band"
+        app:layout_constraintTop_toTopOf="@+id/dialpad_layout"
+        app:layout_constraintHorizontal_chainStyle="packed"
+        app:layout_constraintStart_toEndOf="@+id/center_guideline"
+        android:text="@string/radio_am_text"
+        style="@style/ManualTunerBandButton" />
+    <com.android.car.radio.RadioBandButton
+        android:id="@+id/manual_tuner_fm_band"
+        android:background="@drawable/manual_tuner_band_bg"
+        app:layout_constraintBaseline_toBaselineOf="@+id/manual_tuner_am_band"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toEndOf="@+id/manual_tuner_am_band"
+        android:text="@string/radio_fm_text"
+        style="@style/ManualTunerBandButton" />
 
-                <ImageView
-                    android:id="@+id/exit_manual_tuner_button"
-                    android:background="@drawable/radio_control_background"
-                    android:layout_gravity="center"
-                    android:layout_width="@dimen/stream_button_size"
-                    android:layout_height="@dimen/stream_button_size"
-                    android:scaleType="center"
-                    android:src="@drawable/ic_down_outlined" />
-            </FrameLayout>
-
-            <include
-                android:layout_gravity="center_horizontal"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                layout="@layout/manual_tuner_dialpad" />
-        </FrameLayout>
-
-        <RelativeLayout
-            android:layout_width="@dimen/manual_tuner_container_width"
-            android:layout_height="@dimen/manual_tuner_container_height"
-            android:orientation="vertical">
-
-            <RelativeLayout
-                android:id="@+id/band_selector_container"
-                android:layout_alignParentTop="true"
-                android:layout_centerHorizontal="true"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content" >
-
-                <com.android.car.radio.RadioBandButton
-                    android:id="@+id/manual_tuner_am_band"
-                    android:background="@drawable/radio_control_background"
-                    android:layout_alignParentStart="true"
-                    android:layout_marginEnd="@dimen/radio_default_spacing"
-                    android:text="@string/radio_am_text"
-                    style="@style/ManualTunerBandButton" />
-
-                <com.android.car.radio.RadioBandButton
-                    android:id="@+id/manual_tuner_fm_band"
-                    android:background="@drawable/manual_tuner_band_bg"
-                    android:layout_toEndOf="@id/manual_tuner_am_band"
-                    android:text="@string/radio_fm_text"
-                    style="@style/ManualTunerBandButton" />
-            </RelativeLayout>
-
-            <!-- The layout_above/below combined with a layout_height of match_parent ensures that
-                 this TextView fills the remaining space left by band_selector_container and
-                 the manual_tuner_done_button. -->
-            <TextView
-                android:id="@+id/manual_tuner_channel"
-                android:layout_centerHorizontal="true"
-                android:layout_below="@id/band_selector_container"
-                android:layout_above="@+id/manual_tuner_done_button"
-                android:layout_width="wrap_content"
-                android:layout_height="match_parent"
-                android:gravity="center"
-                style="@style/ManualTunerChannelText" />
-
-            <com.android.car.radio.RadioFabButton
-                android:id="@id/manual_tuner_done_button"
-                android:src="@drawable/ic_done"
-                android:layout_alignParentBottom="true"
-                android:layout_centerHorizontal="true"
-                style="@style/RadioFab" />
-
-            <ImageView
-                android:id="@+id/manual_tuner_backspace"
-                android:background="@drawable/radio_control_background"
-                android:layout_alignBottom="@id/manual_tuner_done_button"
-                android:layout_toEndOf="@id/manual_tuner_done_button"
-                android:layout_width="@dimen/car_radio_fab_size"
-                android:layout_height="@dimen/car_radio_fab_size"
-                android:scaleType="center"
-                android:src="@drawable/ic_backspace" />
-        </RelativeLayout>
-    </LinearLayout>
-
-    <!-- This dividing line is added as the last child so that it has the highest z-index. -->
-    <View
-        android:background="@color/car_list_divider"
-        android:layout_gravity="end|top"
-        android:layout_height="match_parent"
-        android:layout_width="1dp"
-        android:layout_marginEnd="@dimen/manual_tuner_container_width" />
-
-</FrameLayout>
+    <TextView
+        android:id="@+id/manual_tuner_channel"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        app:layout_constraintTop_toBottomOf="@+id/manual_tuner_am_band"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toEndOf="@+id/center_guideline"
+        style="@style/ManualTunerChannelText" />
+    <com.android.car.radio.RadioFabButton
+        android:id="@id/manual_tuner_done_button"
+        android:src="@drawable/ic_done"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toEndOf="@+id/center_guideline"
+        app:layout_constraintTop_toBottomOf="@+id/manual_tuner_channel"
+        style="@style/RadioFab" />
+    <ImageView
+        android:id="@+id/manual_tuner_backspace"
+        android:background="@drawable/radio_control_background"
+        android:layout_width="@dimen/manual_tuner_fab_size"
+        android:layout_height="@dimen/manual_tuner_fab_size"
+        android:scaleType="center"
+        app:layout_constraintStart_toEndOf="@+id/manual_tuner_done_button"
+        app:layout_constraintBottom_toBottomOf="@+id/manual_tuner_done_button"
+        android:src="@drawable/ic_backspace" />
+</android.support.constraint.ConstraintLayout>
diff --git a/res/layout/manual_tuner_dialpad.xml b/res/layout/manual_tuner_dialpad.xml
index 99418f4..4728b8d 100644
--- a/res/layout/manual_tuner_dialpad.xml
+++ b/res/layout/manual_tuner_dialpad.xml
@@ -15,107 +15,94 @@
 -->
 <GridLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
-    android:columnCount="5">
+    android:columnCount="3">
 
     <!-- Row 1 -->
     <Button
         android:id="@+id/manual_tuner_1"
         android:layout_gravity="center"
         android:text="@string/manual_tuner_1"
+        android:layout_columnWeight="1"
+        android:layout_rowWeight="1"
         style="@style/ManualTunerText" />
-    <Space
-        android:layout_width="@dimen/manual_tuner_dialpad_horizontal_spacing"
-        android:layout_height="0dp" />
     <Button
         android:id="@+id/manual_tuner_2"
         android:layout_gravity="center"
         android:text="@string/manual_tuner_2"
+        android:layout_columnWeight="1"
+        android:layout_rowWeight="1"
         style="@style/ManualTunerText" />
-    <Space
-        android:layout_width="@dimen/manual_tuner_dialpad_horizontal_spacing"
-        android:layout_height="0dp" />
     <Button
         android:id="@+id/manual_tuner_3"
         android:layout_gravity="center"
         android:text="@string/manual_tuner_3"
+        android:layout_columnWeight="1"
+        android:layout_rowWeight="1"
         style="@style/ManualTunerText" />
 
-    <Space
-        android:layout_columnSpan="5"
-        android:layout_width="0dp"
-        android:layout_height="@dimen/manual_tuner_dialpad_vertical_spacing" />
-
     <!-- Row 2 -->
     <Button
         android:id="@+id/manual_tuner_4"
         android:layout_gravity="center"
         android:text="@string/manual_tuner_4"
+        android:layout_columnWeight="1"
+        android:layout_rowWeight="1"
         style="@style/ManualTunerText" />
-    <Space
-        android:layout_width="@dimen/manual_tuner_dialpad_horizontal_spacing"
-        android:layout_height="0dp" />
     <Button
         android:id="@+id/manual_tuner_5"
         android:layout_gravity="center"
         android:text="@string/manual_tuner_5"
+        android:layout_columnWeight="1"
+        android:layout_rowWeight="1"
         style="@style/ManualTunerText" />
-    <Space
-        android:layout_width="@dimen/manual_tuner_dialpad_horizontal_spacing"
-        android:layout_height="0dp" />
     <Button
         android:id="@+id/manual_tuner_6"
         android:layout_gravity="center"
         android:text="@string/manual_tuner_6"
+        android:layout_columnWeight="1"
+        android:layout_rowWeight="1"
         style="@style/ManualTunerText" />
 
-    <Space
-        android:layout_columnSpan="5"
-        android:layout_width="0dp"
-        android:layout_height="@dimen/manual_tuner_dialpad_vertical_spacing" />
-
     <!-- Row 3 -->
     <Button
         android:id="@+id/manual_tuner_7"
         android:layout_gravity="center"
         android:text="@string/manual_tuner_7"
+        android:layout_columnWeight="1"
+        android:layout_rowWeight="1"
         style="@style/ManualTunerText" />
-    <Space
-        android:layout_width="@dimen/manual_tuner_dialpad_horizontal_spacing"
-        android:layout_height="0dp" />
     <Button
         android:id="@+id/manual_tuner_8"
         android:layout_gravity="center"
         android:text="@string/manual_tuner_8"
+        android:layout_columnWeight="1"
+        android:layout_rowWeight="1"
         style="@style/ManualTunerText" />
-    <Space
-        android:layout_width="@dimen/manual_tuner_dialpad_horizontal_spacing"
-        android:layout_height="0dp" />
     <Button
         android:id="@+id/manual_tuner_9"
         android:layout_gravity="center"
         android:text="@string/manual_tuner_9"
+        android:layout_columnWeight="1"
+        android:layout_rowWeight="1"
         style="@style/ManualTunerText" />
 
-    <Space
-        android:layout_columnSpan="5"
-        android:layout_width="0dp"
-        android:layout_height="@dimen/manual_tuner_dialpad_vertical_spacing" />
-
     <!-- Row 4 -->
     <Space
-        android:layout_columnSpan="2"
         android:layout_width="0dp"
-        android:layout_height="0dp" />
+        android:layout_height="0dp"
+        android:layout_columnWeight="1"
+        android:layout_rowWeight="1"/>
     <Button
         android:id="@+id/manual_tuner_0"
         android:layout_gravity="center"
         android:text="@string/manual_tuner_0"
+        android:layout_columnWeight="1"
+        android:layout_rowWeight="1"
         style="@style/ManualTunerText" />
     <Space
-        android:layout_columnSpan="2"
         android:layout_width="0dp"
-        android:layout_height="0dp" />
+        android:layout_height="0dp"
+        android:layout_columnWeight="1"
+        android:layout_rowWeight="1"/>
 
 </GridLayout>
diff --git a/res/layout/radio_channel.xml b/res/layout/radio_channel.xml
deleted file mode 100644
index f973196..0000000
--- a/res/layout/radio_channel.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<RelativeLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_alignParentTop="true"
-    android:layout_alignParentEnd="true"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
-    android:orientation="horizontal">
-
-    <TextView
-        android:id="@+id/radio_list_station_channel"
-        android:layout_marginEnd="@dimen/stream_content_keyline_1"
-        android:layout_alignParentEnd="true"
-        android:layout_alignParentTop="true"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        style="@style/RadioStationChannelText" />
-
-    <!-- Using a negative margin start on this View so that it starts off flushed to the
-         end of radio_station_channel. This is needed because radio_station_channel needs
-         to be a distance of stream_content_keyline_2 from the end of its parent. -->
-    <TextView
-        android:id="@+id/radio_list_station_band"
-        android:layout_alignBaseline="@id/radio_list_station_channel"
-        android:layout_gravity="bottom|end"
-        android:layout_toEndOf="@id/radio_list_station_channel"
-        android:layout_marginStart="@dimen/stream_content_keyline_1_neg"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:paddingStart="@dimen/car_radio_band_display_margin_start"
-        style="@style/RadioStationBandText" />
-</RelativeLayout>
diff --git a/res/layout/radio_channel_list.xml b/res/layout/radio_channel_list.xml
deleted file mode 100644
index da1167a..0000000
--- a/res/layout/radio_channel_list.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<com.android.car.radio.CarouselView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/radio_station_list"
-    android:layout_width="wrap_content"
-    android:layout_height="match_parent"
-    app:itemMargins="@dimen/car_radio_channel_item_margin" />
diff --git a/res/layout/radio_controls.xml b/res/layout/radio_controls.xml
index 6505376..8c1e513 100644
--- a/res/layout/radio_controls.xml
+++ b/res/layout/radio_controls.xml
@@ -42,14 +42,13 @@
 
     <com.android.car.radio.PlayPauseButton
         android:id="@+id/radio_play_button"
-        android:transitionName="@string/radio_play_button_transition_name"
         android:layout_gravity="center"
-        android:layout_width="@dimen/stream_fab_size"
-        android:layout_height="@dimen/stream_fab_size"
+        android:layout_width="0dp"
+        android:layout_height="@dimen/car_radio_controls_button_size"
+        android:layout_weight="1"
         android:scaleType="center"
         android:src="@drawable/ic_play_pause_stop"
-        android:elevation="@dimen/car_radio_fab_elevation"
-        android:stateListAnimator="@anim/car_fab_state_list_animator" />
+        style="@style/RadioButtonFab" />
 
     <ImageButton
         android:id="@+id/radio_forward_button"
@@ -67,6 +66,6 @@
         android:layout_height="@dimen/car_radio_controls_button_size"
         android:layout_weight="1"
         android:src="@drawable/ic_star_empty"
-        style="@style/RadioButton" />
+        style="@style/RadioButtonPreset" />
 
 </LinearLayout>
diff --git a/res/layout/radio_fragment.xml b/res/layout/radio_fragment.xml
index 58e930c..87de2ad 100644
--- a/res/layout/radio_fragment.xml
+++ b/res/layout/radio_fragment.xml
@@ -31,7 +31,7 @@
         android:gravity="center"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:paddingBottom="@dimen/action_panel_height"
+        android:paddingBottom="@dimen/car_action_bar_height"
         android:visibility="gone"
         android:orientation="vertical" >
 
@@ -58,65 +58,49 @@
         android:layout_height="match_parent"
         android:orientation="vertical" >
 
-        <RelativeLayout
+        <LinearLayout
             android:id="@+id/radio_station_display_container"
             android:layout_width="match_parent"
             android:layout_height="0dp"
             android:layout_weight="1"
-            android:paddingLeft="@dimen/stream_content_keyline_1" >
+            android:gravity="center"
+            android:orientation="vertical">
 
-            <LinearLayout
-                android:layout_alignParentStart="true"
-                android:layout_width="@dimen/car_radio_metadata_container_width"
-                android:layout_height="wrap_content"
-                android:layout_marginTop="@dimen/lens_header_height"
-                android:paddingTop="@dimen/car_radio_container_top_padding"
-                android:orientation="vertical">
-
-                <TextView
-                    android:id="@+id/radio_station_song"
-                    android:ellipsize="end"
-                    android:layout_gravity="bottom"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:maxLines="@integer/radio_metadata1_max_lines"
-                    android:textColor="@color/car_headline1_light"
-                    android:textSize="@dimen/car_headline1_size"
-                    android:fontFamily="sans-serif-medium"
-                    android:visibility="gone" />
-
-                <TextView
-                    android:id="@+id/radio_station_artist_or_station"
-                    android:ellipsize="end"
-                    android:layout_gravity="bottom"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:maxLines="@integer/radio_metadata2_max_lines"
-                    android:textColor="@color/car_headline1_light"
-                    android:textSize="@dimen/car_headline1_size"
-                    android:visibility="gone" />
-            </LinearLayout>
-
-            <ViewStub
-                android:id="@+id/channel_list_view_stub"
-                android:layout="@layout/radio_channel_list"
+            <TextView
+                android:id="@+id/radio_station_name"
+                android:ellipsize="end"
+                android:layout_gravity="center"
                 android:layout_width="wrap_content"
-                android:layout_height="wrap_content" />
-
+                android:layout_height="wrap_content"
+                android:maxLines="@integer/radio_metadata2_max_lines"
+                android:textColor="@color/car_headline1_light"
+                android:textSize="@dimen/car_headline1_size"
+                android:visibility="invisible" />
             <ViewStub
                 android:id="@+id/single_channel_view_stub"
                 android:layout="@layout/radio_single_channel"
-                android:layout_marginTop="@dimen/lens_header_height"
+                android:layout_gravity="center"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content" />
-        </RelativeLayout>
+            <TextView
+                android:id="@+id/radio_station_song_artist"
+                android:ellipsize="end"
+                android:layout_gravity="center"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:maxLines="@integer/radio_metadata1_max_lines"
+                android:textColor="@color/car_headline1_light"
+                android:textSize="@dimen/car_headline1_size"
+                android:fontFamily="sans-serif-medium"
+                android:visibility="invisible" />
+        </LinearLayout>
 
         <android.support.v7.widget.CardView
             android:id="@+id/radio_layout_container"
             android:layout_gravity="center_horizontal"
             android:transitionName="@string/radio_controls_transition_name"
             android:layout_width="match_parent"
-            android:layout_height="@dimen/action_panel_height"
+            android:layout_height="@dimen/car_action_bar_height"
             app:cardBackgroundColor="@color/car_card" >
 
             <include layout="@layout/radio_controls" />
diff --git a/res/layout/radio_preset_item.xml b/res/layout/radio_preset_item.xml
index 6051a44..e2b0502 100644
--- a/res/layout/radio_preset_item.xml
+++ b/res/layout/radio_preset_item.xml
@@ -19,20 +19,22 @@
     android:layout_height="@dimen/car_preset_item_height"
     android:orientation="horizontal" >
 
-    <!-- This FrameLayout ensures that the preset_item_metadata starts at stream_card_keyline_3
-         while the radio_station_channels starts at stream_card_keyline_1. -->
     <FrameLayout
-        android:layout_width="@dimen/stream_card_keyline_3"
-        android:layout_height="match_parent">
-
+        android:id="@+id/preset_station_background"
+        android:layout_marginStart="@dimen/car_keyline_1"
+        android:layout_marginEnd="@dimen/car_keyline_1"
+        android:layout_gravity="center_vertical"
+        android:layout_width="@dimen/car_preset_number_bg_width"
+        android:layout_height="@dimen/car_preset_number_bg_height"
+        android:background="@drawable/preset_channel_bg" >
         <TextView
             android:id="@+id/preset_station_channel"
-            android:background="@drawable/preset_channel_bg"
-            android:layout_marginStart="@dimen/stream_card_keyline_1"
-            android:layout_width="@dimen/car_preset_number_bg_width"
-            android:layout_height="@dimen/car_preset_number_bg_height"
-            android:layout_gravity="center_vertical"
+            android:layout_gravity="center"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
             android:gravity="center"
+            android:drawablePadding="@dimen/car_preset_equalizer_padding"
+            android:maxLines="1"
             android:textSize="@dimen/car_body2_size"
             style="@style/RadioPresetItemChannelText" />
     </FrameLayout>
@@ -46,13 +48,14 @@
         android:layout_marginEnd="@dimen/radio_default_spacing"
         style="@style/RadioPresetItemMetadataText" />
 
-    <ImageView
-        android:id="@+id/preset_equalizer"
-        android:layout_width="@dimen/preset_equalizer_size"
-        android:layout_height="@dimen/preset_equalizer_size"
-        android:layout_gravity="center_vertical"
-        android:layout_marginEnd="@dimen/stream_card_keyline_1"
-        android:src="@drawable/ic_equalizer"
-        android:visibility="gone" />
+    <ImageButton
+        android:id="@+id/preset_button"
+        android:layout_marginEnd="@dimen/car_keyline_1"
+        android:layout_gravity="center_vertical|right"
+        android:layout_width="@dimen/car_preset_favorite_button_size"
+        android:layout_height="@dimen/car_preset_favorite_button_size"
+        android:gravity="right"
+        android:src="@drawable/ic_star_filled"
+        style="@style/RadioButton" />
 
 </LinearLayout>
diff --git a/res/layout/radio_preset_stream_card.xml b/res/layout/radio_preset_stream_card.xml
index 669a03b..2f53d41 100644
--- a/res/layout/radio_preset_stream_card.xml
+++ b/res/layout/radio_preset_stream_card.xml
@@ -21,7 +21,7 @@
     android:layout_width="match_parent"
     android:layout_height="wrap_content" >
 
-    <com.android.car.stream.ui.StreamCardView
+    <androidx.car.widget.ColumnCardView
         android:id="@+id/preset_card"
         android:layout_gravity="center"
         android:layout_width="wrap_content"
@@ -29,5 +29,5 @@
         style="@style/RadioPresetCard" >
 
         <include layout="@layout/radio_preset_item" />
-    </com.android.car.stream.ui.StreamCardView>
+    </androidx.car.widget.ColumnCardView>
 </FrameLayout>
diff --git a/res/layout/radio_presets_list.xml b/res/layout/radio_presets_list.xml
index 06ce6ab..e67b038 100644
--- a/res/layout/radio_presets_list.xml
+++ b/res/layout/radio_presets_list.xml
@@ -41,24 +41,23 @@
             android:layout_height="match_parent" />
     </FrameLayout>
 
-    <com.android.car.view.PagedListView
+    <androidx.car.widget.PagedListView
         android:id="@+id/presets_list"
         android:background="@android:color/transparent"
         android:layout_marginTop="@dimen/car_preset_container_height"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:visibility="gone"
-        app:offsetRows="true"
-        app:showDivider="true"
-        app:dividerStartMargin="@dimen/stream_card_keyline_3"
-        app:alignDividerEndTo="@id/preset_card"
-        app:alignDividerStartTo="@id/preset_card" />
+        app:showPagedListViewDivider="true"
+        app:dividerStartMargin="@dimen/car_keyline_3"
+        app:alignDividerEndTo="@id/current_radio_station_card_controls"
+        app:alignDividerStartTo="@id/current_radio_station_card_controls" />
 
     <android.support.v7.widget.CardView
         android:id="@+id/current_radio_station_card"
         android:layout_gravity="bottom|center_horizontal"
         android:layout_width="match_parent"
-        android:layout_height="@dimen/action_panel_height"
+        android:layout_height="@dimen/car_action_bar_height"
         android:foreground="@drawable/radio_control_background"
         app:cardBackgroundColor="@color/car_card" >
 
@@ -72,12 +71,12 @@
             android:alpha="0" >
 
             <!-- This wrapping RelativeLayout ensures both the band and channel are centered
-                 within the StreamCardView. A RelativeLayout is used instead of something
+                 within the CardView. A RelativeLayout is used instead of something
                  more light-weight so that the band can align to the baseline of the
                  channel. -->
             <RelativeLayout
                 android:layout_alignParentStart="true"
-                android:layout_marginStart="@dimen/stream_card_keyline_1"
+                android:layout_marginStart="@dimen/car_keyline_1"
                 android:layout_width="wrap_content"
                 android:layout_height="match_parent">
 
@@ -106,8 +105,8 @@
             <com.android.car.radio.PlayPauseButton
                 android:id="@+id/preset_radio_play_button"
                 android:layout_centerInParent="true"
-                android:layout_width="@dimen/stream_fab_size"
-                android:layout_height="@dimen/stream_fab_size"
+                android:layout_width="@dimen/car_radio_controls_fab_size"
+                android:layout_height="@dimen/car_radio_controls_fab_size"
                 android:padding="@dimen/car_presets_play_button_padding"
                 android:scaleType="centerInside"
                 android:src="@drawable/ic_play_pause_stop"
diff --git a/res/layout/radio_single_channel.xml b/res/layout/radio_single_channel.xml
index d5af711..b521488 100644
--- a/res/layout/radio_single_channel.xml
+++ b/res/layout/radio_single_channel.xml
@@ -24,23 +24,20 @@
 
     <TextView
         android:id="@+id/radio_station_channel"
-        android:layout_marginEnd="@dimen/stream_content_keyline_1"
+        android:layout_marginEnd="@dimen/car_keyline_1"
         android:layout_marginTop="@dimen/car_radio_station_top_margin"
-        android:layout_alignParentEnd="true"
-        android:layout_alignParentTop="true"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         style="@style/RadioStationChannelText" />
 
     <!-- Using a negative margin start on this View so that it starts off flushed to the
          end of radio_station_channel. This is needed because radio_station_channel needs
-         to be a distance of stream_content_keyline_2 from the end of its parent. -->
+         to be a distance of car_keyline_2 from the end of its parent. -->
     <TextView
         android:id="@+id/radio_station_band"
         android:layout_alignBaseline="@id/radio_station_channel"
-        android:layout_gravity="bottom|end"
         android:layout_toEndOf="@id/radio_station_channel"
-        android:layout_marginStart="@dimen/stream_content_keyline_1_neg"
+        android:layout_marginStart="@dimen/car_keyline_1_neg"
         android:layout_marginTop="@dimen/car_radio_station_top_margin"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
diff --git a/res/values-h480dp/dimens.xml b/res/values-h480dp/dimens.xml
index 9dd759b..3ad7475 100644
--- a/res/values-h480dp/dimens.xml
+++ b/res/values-h480dp/dimens.xml
@@ -15,4 +15,5 @@
 -->
 <resources>
     <dimen name="manual_tuner_container_height">368dp</dimen>
+    <dimen name="car_radio_controls_fab_size">100dp</dimen>
 </resources>
diff --git a/res/values-night/colors.xml b/res/values-night/colors.xml
index 395670f..5162166 100644
--- a/res/values-night/colors.xml
+++ b/res/values-night/colors.xml
@@ -20,7 +20,6 @@
     <color name="car_radio_control_fab_button_disabled">@color/car_grey_600</color>
     <color name="car_radio_display_scrim">#52000000</color>  <!-- 32% black -->
     <color name="radio_preset_item_text">@color/car_grey_100</color>
-    <color name="radio_ripple_background">@color/car_card_ripple_background_light</color>
     <color name="manual_tuner_button_text">@color/car_grey_100</color>
     <color name="manual_tuner_channel_text">@color/car_grey_100</color>
     <color name="manual_tuner_done_disabled">@color/car_grey_800</color>
diff --git a/res/values-w1024dp/dimens.xml b/res/values-w1024dp/dimens.xml
index 3bdc28b..96f1d12 100644
--- a/res/values-w1024dp/dimens.xml
+++ b/res/values-w1024dp/dimens.xml
@@ -15,6 +15,6 @@
 -->
 <resources>
     <dimen name="car_radio_metadata_container_width">484dp</dimen>
-    <dimen name="manual_tuner_back_button_container_size">@dimen/stream_content_keyline_1</dimen>
+    <dimen name="manual_tuner_back_button_container_size">@dimen/car_keyline_1</dimen>
     <dimen name="manual_tuner_container_width">450dp</dimen>
 </resources>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 849cf12..f92e004 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -14,34 +14,12 @@
 limitations under the License.
 -->
 <resources>
-    <!-- Attributes that can be customized on a com.android.car.radio.RadioPresetButton. -->
-    <declare-styleable name="RadioPresetButton">
-        <!-- The station that is currently set on this preset (e.g. 96.5). -->
-        <attr name="presetStation" format="string" />
-
-        <!-- The preset title. This is usually the number of the preset. For example, a radio
-             with 5 presets could have buttons with the numbers 01 - 05. -->
-        <attr name="presetTitle" format="string" />
-
-        <!-- Whether or not this preset button will display the station that is currently set on it. -->
-        <attr name="presetDisplayEnabled" format="boolean" />
-
-        <!-- Whether or not this preset is currently selected. -->
-        <attr name="presetSelected" format="boolean" />
-    </declare-styleable>
-
     <!-- Attributes for the playing state of com.android.car.radio.PlayPauseButton. -->
     <declare-styleable name="PlayingState">
         <attr name="state_paused" format="boolean"/>
         <attr name="state_playing" format="boolean"/>
     </declare-styleable>
 
-    <!-- Attributes for customizing a CaoruselView. -->
-    <declare-styleable name="CarouselView">
-        <attr name="topOffset" format="dimension" />
-        <attr name="itemMargins" format="dimension" />
-    </declare-styleable>
-
     <!-- Attributes for customizing a RadioBandButton. -->
     <declare-styleable name="RadioBandButton">
         <attr name="isBandSelected" format="boolean" />
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 6bcfb10..957f75b 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -32,9 +32,8 @@
 
     <!-- The color of the radio control buttons. -->
     <color name="car_radio_control_button">@android:color/black</color>
-
-    <!-- The ripple color for various radio buttons and cards. -->
-    <color name="radio_ripple_background">@color/car_card_ripple_light_color_background_dark</color>
+    <!-- The color of the radio control buttons. -->
+    <color name="car_radio_equalizer">@android:color/white</color>
 
     <!-- The color of the radio control buttons when they are disabled. -->
     <color name="car_radio_control_button_disabled">@color/car_grey_400</color>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 7b89504..08920d5 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -14,8 +14,8 @@
 limitations under the License.
 -->
 <resources>
-    <!-- The default size for all FABs in the radio. -->
-    <dimen name="car_radio_fab_size">160dp</dimen>
+    <!-- The size of the manual tuner FAB. -->
+    <dimen name="manual_tuner_fab_size">160dp</dimen>
 
     <!-- The default spacing for padding and margins in the radio application. -->
     <dimen name="radio_default_spacing">16dp</dimen>
@@ -66,12 +66,12 @@
     <dimen name="car_preset_item_radius">64dp</dimen>
 
     <!-- The dimensions of the pill background behind each preset channel. -->
-    <dimen name="car_preset_number_bg_width">112dp</dimen>
+    <dimen name="car_preset_number_bg_width">160dp</dimen>
     <dimen name="car_preset_number_bg_height">76dp</dimen>
 
     <!-- The radius of the pill. This number should be half the value of
          car_preset_number_bg_height. -->
-    <dimen name="car_preset_number_bg_radius">38dp</dimen>
+    <dimen name="car_preset_number_bg_radius">8dp</dimen>
 
     <!-- The total height of the container that holds the current radio card in the preset list. -->
     <dimen name="car_preset_container_height">176dp</dimen>
@@ -82,6 +82,9 @@
     <!-- The size of the text displaying the current channel number in the presets activity. -->
     <dimen name="car_preset_current_channel_text_size">46sp</dimen>
 
+    <!-- The space between the currently playing icon and the radio band -->
+    <dimen name="car_preset_equalizer_padding">8dp</dimen>
+
     <!-- The spacing between the current radio channel and the current radio band in the preset
          list. -->
     <dimen name="car_preset_current_channel_separator">8dp</dimen>
@@ -92,13 +95,8 @@
     <!-- The padding of the play button in the radio in the presets activity. -->
     <dimen name="car_presets_play_button_padding">16dp</dimen>
 
-    <!-- The corner radius for the cards in the preset list. This corner is only present on the
-         first and last card in the list and on the top and bottom respectively. -->
-    <dimen name="car_preset_card_radius">16dp</dimen>
-
-    <!-- The size of the equalizer next to a preset item that indicates it is the currently
-         playing radio station. -->
-    <dimen name="preset_equalizer_size">60dp</dimen>
+    <!-- The absolute size of the favorite button in the radio in the presets activity. -->
+    <dimen name="car_preset_favorite_button_size">64dp</dimen>
 
     <!-- The size of the icon indicating that there is an error with loading the radio. -->
     <dimen name="radio_error_icon_size">146dp</dimen>
@@ -116,7 +114,7 @@
     <dimen name="manual_tuner_channel_text_size">96sp</dimen>
 
     <!-- The height of the container corresponding to manual_tuner_container_width. This height
-         should equal to the height of the dialpad. This is equal to 4 * stream_button_size +
+         should equal to the height of the dialpad. This is equal to 4 * car_touch_target +
          3 * manual_tuner_dialpad_vertical_spacing. -->
     <dimen name="manual_tuner_container_height">288dp</dimen>
 
@@ -126,19 +124,13 @@
 
     <!-- The top padding before the content within the manual tuner. -->
     <dimen name="manual_tuner_top_padding">@dimen/radio_default_spacing</dimen>
+    <!-- The left padding before the content within the manual tuner. -->
+    <dimen name="manual_tuner_left_padding">@dimen/radio_default_spacing</dimen>
 
     <!-- The size of the numbers in the manual tuner dialpad. -->
-    <dimen name="manual_tuner_dialpad_text_size">50sp</dimen>
+    <dimen name="manual_tuner_dialpad_text_size">64sp</dimen>
 
     <!-- The top margin for the dialpad. This value is negative to pull the dialpad up a bit
          for visual purposes. -->
     <dimen name="manual_tuner_dialpad_top_margin">-16dp</dimen>
-
-    <!-- The spacing between the buttons on the dialpad. -->
-    <dimen name="manual_tuner_dialpad_horizontal_spacing">56dp</dimen>
-    <dimen name="manual_tuner_dialpad_vertical_spacing">0dp</dimen>
-
-    <!-- The size of the container that houses the back button in the manual tuner. The back button
-         is centered within this container. -->
-    <dimen name="manual_tuner_back_button_container_size">@dimen/stream_content_keyline_2</dimen>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index b71651d..087604f 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -17,9 +17,6 @@
     <!-- The main application name for the radio. -->
     <string name="app_name">Radio</string>
 
-    <!-- The name for the service that is responsible for connecting to the radio API. -->
-    <string name="service_name">RadioService</string>
-
     <!-- Text to denote the AM radio band. -->
     <string name="radio_am_text">AM</string>
 
@@ -39,6 +36,10 @@
     <!-- Text indicating to the user that there was an error loading the error. -->
     <string name="radio_problem_text">Radio not available</string>
 
+    <!-- Error message after invalid MediaSession API selection (mainly by other apps,
+         like assistant). -->
+    <string name="invalid_selection">Invalid selection</string>
+
     <!-- Default metadata text if a preset has no metadata. This text should indicate to the user
          that the station is favorited followed by the number of the favorite. -->
     <string name="radio_default_preset_metadata_text">Favorite <xliff:g>%1$s</xliff:g></string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index fbd33ce..abb3057 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -21,6 +21,22 @@
         <item name="android:scaleType">center</item>
     </style>
 
+    <!-- The radio button style in presets display. -->
+    <style name="RadioButtonPreset" parent="@android:style/Widget.Material.Button.Borderless">
+        <item name="android:background">@drawable/radio_control_background</item>
+        <item name="android:scaleType">center</item>
+    </style>
+
+    <!-- The radio button style in presets display. -->
+    <style name="RadioButtonFab" parent="@android:style/Widget.Material.Button.Borderless">
+        <item name="android:background">@drawable/radio_control_background</item>
+        <item name="android:padding">@dimen/car_radio_controls_button_padding</item>
+        <item name="android:scaleType">center</item>
+        <item name="android:transitionName">@string/radio_play_button_transition_name</item>
+        <item name="android:elevation">@dimen/car_radio_fab_elevation</item>
+        <item name="android:stateListAnimator">@anim/car_fab_state_list_animator</item>
+    </style>
+
     <!-- The text style for the radio channel in the main display. -->
     <style name="RadioStationChannelText">
         <item name="android:fontFamily">sans-serif-light</item>
@@ -70,8 +86,8 @@
 
     <!-- The default style for FAB icons in the radio. -->
     <style name="RadioFab">
-        <item name="android:layout_width">@dimen/car_radio_fab_size</item>
-        <item name="android:layout_height">@dimen/car_radio_fab_size</item>
+        <item name="android:layout_width">@dimen/manual_tuner_fab_size</item>
+        <item name="android:layout_height">@dimen/manual_tuner_fab_size</item>
         <item name="android:padding">@dimen/car_radio_fab_padding</item>
         <item name="android:scaleType">centerCrop</item>
         <item name="android:elevation">@dimen/car_radio_fab_elevation</item>
@@ -80,8 +96,8 @@
     <!-- The style for the text in the manual tuner dialpad. -->
     <style name="ManualTunerText">
         <item name="android:background">@drawable/radio_control_background</item>
-        <item name="android:layout_height">@dimen/stream_button_size</item>
-        <item name="android:layout_width">@dimen/stream_button_size</item>
+        <item name="android:layout_height">@dimen/car_touch_target_size</item>
+        <item name="android:layout_width">@dimen/car_touch_target_size</item>
         <item name="android:gravity">center</item>
         <item name="android:textColor">@color/manual_tuner_button_text</item>
         <item name="android:textSize">@dimen/manual_tuner_dialpad_text_size</item>
@@ -100,7 +116,7 @@
     <!-- The text style for the text displaying the current channel that the user has typed. -->
     <style name="ManualTunerChannelText">
         <item name="android:textAllCaps">true</item>
-        <item name="android:textColor">@color/car_headline0</item>
+        <item name="android:textColor">@color/car_headline1</item>
         <item name="android:textSize">@dimen/manual_tuner_channel_text_size</item>
     </style>
 </resources>
diff --git a/res/values/themes.xml b/res/values/themes.xml
index f48648b..12aaef9 100644
--- a/res/values/themes.xml
+++ b/res/values/themes.xml
@@ -15,7 +15,10 @@
 -->
 <resources>
     <!-- The theme of the main radio display. -->
-    <style name="CarRadioAppTheme" parent="CarDrawerActivityTheme">
+    <style name="CarRadioAppTheme" parent="Theme.Car.Light.NoActionBar.Drawer">
+        <item name="android:colorAccent">@color/car_radio_accent_color</item>
+        <item name="android:colorPrimary">@android:color/transparent</item>
+        <item name="android:colorPrimaryDark">@color/car_radio_bg_color</item>
         <item name="android:statusBarColor">@color/car_radio_bg_color</item>
         <item name="colorAccent">@color/car_radio_accent_color</item>
     </style>
diff --git a/src/com/android/car/radio/BootupReceiver.java b/src/com/android/car/radio/BootupReceiver.java
deleted file mode 100644
index facbcd7..0000000
--- a/src/com/android/car/radio/BootupReceiver.java
+++ /dev/null
@@ -1,41 +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.car.radio;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.util.Log;
-
-/**
- * {@link BroadcastReceiver} that will start the {@link RadioService} when the system has finished
- * booting.
- */
-public class BootupReceiver extends BroadcastReceiver {
-    private static final String TAG = "Em.RadioBootReceiver";
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "ACTION_BOOT_COMPLETED");
-            }
-
-            context.startService(new Intent(context, RadioService.class));
-        }
-    }
-}
diff --git a/src/com/android/car/radio/CarRadioActivity.java b/src/com/android/car/radio/CarRadioActivity.java
index c278bbc..642e995 100644
--- a/src/com/android/car/radio/CarRadioActivity.java
+++ b/src/com/android/car/radio/CarRadioActivity.java
@@ -16,18 +16,19 @@
 
 package com.android.car.radio;
 
+import android.annotation.Nullable;
 import android.content.Intent;
+import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
 import android.os.Bundle;
-import android.support.annotation.Nullable;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
 import android.util.Log;
+import android.util.Pair;
 
-import com.android.car.app.CarDrawerActivity;
-import com.android.car.app.CarDrawerAdapter;
-import com.android.car.app.DrawerItemViewHolder;
-import com.android.car.radio.service.RadioStation;
+import androidx.car.drawer.CarDrawerActivity;
+import androidx.car.drawer.CarDrawerAdapter;
+import androidx.car.drawer.DrawerItemViewHolder;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -44,8 +45,7 @@
     private static final String MANUAL_TUNER_BACKSTACK = "MANUAL_TUNER_BACKSTACK";
     private static final String CONTENT_FRAGMENT_TAG = "CONTENT_FRAGMENT_TAG";
 
-    private static final int[] SUPPORTED_RADIO_BANDS = new int[] {
-        RadioManager.BAND_AM, RadioManager.BAND_FM };
+    private static final List<Pair<Integer, String>> SUPPORTED_RADIO_BANDS = new ArrayList<>();
 
     /**
      * Intent action for notifying that the radio state has changed.
@@ -73,7 +73,13 @@
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
+        SUPPORTED_RADIO_BANDS.add(
+                new Pair<>(RadioManager.BAND_AM, getString(R.string.radio_am_text)));
+        SUPPORTED_RADIO_BANDS.add(
+                new Pair<>(RadioManager.BAND_FM, getString(R.string.radio_fm_text)));
+
         super.onCreate(savedInstanceState);
+        setToolbarElevation(0f);
 
         mRadioController = new RadioController(this);
         setContentFragment(
@@ -121,7 +127,7 @@
     }
 
     @Override
-    public void onStationSelected(RadioStation station) {
+    public void onStationSelected(ProgramSelector sel) {
         maybeDismissManualTuner();
 
         Fragment fragment = getCurrentFragment();
@@ -129,8 +135,8 @@
             ((FragmentWithFade) fragment).fadeInContent();
         }
 
-        if (station != null) {
-            mRadioController.tuneToRadioChannel(station);
+        if (sel != null) {
+            mRadioController.tune(sel);
         }
     }
 
@@ -223,16 +229,14 @@
      */
     private class RadioDrawerAdapter extends CarDrawerAdapter {
         private final List<String> mDrawerOptions =
-                new ArrayList<>(SUPPORTED_RADIO_BANDS.length + 1);
+                new ArrayList<>(SUPPORTED_RADIO_BANDS.size() + 1);
 
         RadioDrawerAdapter() {
             super(CarRadioActivity.this, false /* showDisabledListOnEmpty */);
             setTitle(getString(R.string.app_name));
             // The ordering of options is hardcoded. The click handler below depends on it.
-            for (int band : SUPPORTED_RADIO_BANDS) {
-                String bandText =
-                        RadioChannelFormatter.formatRadioBand(CarRadioActivity.this, band);
-                mDrawerOptions.add(bandText);
+            for (Pair<Integer, String> band : SUPPORTED_RADIO_BANDS) {
+                mDrawerOptions.add(band.second);
             }
             mDrawerOptions.add(getString(R.string.manual_tuner_drawer_entry));
         }
@@ -249,10 +253,10 @@
 
         @Override
         public void onItemClick(int position) {
-            closeDrawer();
-            if (position < SUPPORTED_RADIO_BANDS.length) {
-                mRadioController.openRadioBand(SUPPORTED_RADIO_BANDS[position]);
-            } else if (position == SUPPORTED_RADIO_BANDS.length) {
+            getDrawerController().closeDrawer();
+            if (position < SUPPORTED_RADIO_BANDS.size()) {
+                mRadioController.switchBand(SUPPORTED_RADIO_BANDS.get(position).first);
+            } else if (position == SUPPORTED_RADIO_BANDS.size()) {
                 startManualTuner();
             } else {
                 Log.w(TAG, "Unexpected position: " + position);
diff --git a/src/com/android/car/radio/CarouselView.java b/src/com/android/car/radio/CarouselView.java
deleted file mode 100644
index 2fc5ec0..0000000
--- a/src/com/android/car/radio/CarouselView.java
+++ /dev/null
@@ -1,477 +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.car.radio;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.database.Observable;
-import android.util.AttributeSet;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-
-import java.util.ArrayList;
-
-/**
- * A View that displays a vertical list of child views provided by a {@link CarouselView.Adapter}.
- * The Views can be shifted up and down and will loop backwards on itself if the end is reached.
- * The View that is considered first to be displayed can be offset by a given amount, and the rest
- * of the Views will sandwich that first View.
- */
-public class CarouselView extends ViewGroup {
-    private static final String TAG = "CarouselView";
-
-    /**
-     * The alpha is that is used for the view considered first in the carousel.
-     */
-    private static final float FIRST_VIEW_ALPHA = 1.f;
-
-    /**
-     * The alpha for all the other views in the carousel.
-     */
-    private static final float DEFAULT_VIEW_ALPHA = 0.24f;
-
-    private CarouselView.Adapter mAdapter;
-    private int mTopOffset;
-    private int mItemMargin;
-
-    /**
-     * The position into the the data set in {@link #mAdapter} that will be displayed as the first
-     * item in the carousel.
-     */
-    private int mStartPosition;
-
-    /**
-     * The number of views in {@link #mScrapViews} that have been bound with data and should be
-     * displayed in the carousel. This number can be different from the size of {@code mScrapViews}.
-     */
-    private int mBoundViews;
-
-    /**
-     * A {@link ArrayList} of scrap Views that can be used to populate the carousel. The views
-     * contained in this scrap will be the ones that are returned {@link #mAdapter}.
-     */
-    private ArrayList<View> mScrapViews = new ArrayList<>();
-
-    public CarouselView(Context context) {
-        super(context);
-        init(context, null);
-    }
-
-    public CarouselView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        init(context, attrs);
-    }
-
-    public CarouselView(Context context, AttributeSet attrs, int defStyleAttrs) {
-        super(context, attrs, defStyleAttrs);
-        init(context, attrs);
-    }
-
-    public CarouselView(Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
-        super(context, attrs, defStyleAttrs, defStyleRes);
-        init(context, attrs);
-    }
-
-    /**
-     * Initializes the starting top offset and margins between each of the items in the carousel.
-     */
-    private void init(Context context, AttributeSet attrs) {
-        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CarouselView);
-
-        try {
-            setTopOffset(ta.getDimensionPixelSize(R.styleable.CarouselView_topOffset, 0));
-            setItemMargins(ta.getDimensionPixelSize(R.styleable.CarouselView_itemMargins, 0));
-        } finally {
-            ta.recycle();
-        }
-    }
-
-    /**
-     * Sets the adapter that will provide the Views to be displayed in the carousel.
-     */
-    public void setAdapter(CarouselView.Adapter adapter) {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "setAdapter(): " + adapter);
-        }
-
-        if (mAdapter != null) {
-            mAdapter.unregisterAll();
-        }
-
-        mAdapter = adapter;
-
-        // Clear the scrap views because the Views returned from the adapter can be different from
-        // an adapter that was previously set.
-        mScrapViews.clear();
-
-        if (mAdapter != null) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "adapter item count: " + adapter.getItemCount());
-            }
-
-            mScrapViews.ensureCapacity(adapter.getItemCount());
-            mAdapter.registerObserver(this);
-        }
-    }
-
-    /**
-     * Sets the amount by which the first view in the carousel will be offset from the top of the
-     * carousel. The last item and second item will sandwich this first view and expand upwards
-     * and downwards respectively as space permits.
-     *
-     * <p>This value can be set in XML with the value {@code app:topOffset}.
-     */
-    public void setTopOffset(int topOffset) {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "setTopOffset(): " + topOffset);
-        }
-
-        mTopOffset = topOffset;
-    }
-
-    /**
-     * Sets the amount of space between each item in the carousel.
-     *
-     * <p>This value can be set in XML with the value {@code app:itemMargins}.
-     */
-    public void setItemMargins(int itemMargin) {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "setItemMargins(): " + itemMargin);
-        }
-
-        mItemMargin = itemMargin;
-    }
-
-    /**
-     * Shifts the carousel to the specified position.
-     */
-    public void shiftToPosition(int position) {
-        if (mAdapter == null || position >= mAdapter.getItemCount() || position < 0) {
-            return;
-        }
-
-        mStartPosition = position;
-        requestLayout();
-    }
-
-    @Override
-    protected void onMeasure(int widthSpec, int heightSpec) {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "onMeasure()");
-        }
-
-        removeAllViewsInLayout();
-
-        // If there is no adapter, then have the carousel take up no space.
-        if (mAdapter == null) {
-            Log.w(TAG, "No adapter set on this CarouselView. "
-                    + "Setting measured dimensions as (0, 0)");
-            setMeasuredDimension(0, 0);
-            return;
-        }
-
-        int widthMode = MeasureSpec.getMode(widthSpec);
-        int heightMode = MeasureSpec.getMode(heightSpec);
-
-        int requestedHeight;
-        if (heightMode == MeasureSpec.UNSPECIFIED) {
-            requestedHeight = getDefaultHeight();
-        } else {
-            requestedHeight = MeasureSpec.getSize(heightSpec);
-        }
-
-        int requestedWidth;
-        if (widthMode == MeasureSpec.UNSPECIFIED) {
-            requestedWidth = getDefaultWidth();
-        } else {
-            requestedWidth = MeasureSpec.getSize(widthSpec);
-        }
-
-        // The children of this carousel can take up as much space as this carousel has been
-        // set to.
-        int childWidthSpec = MeasureSpec.makeMeasureSpec(requestedWidth, MeasureSpec.AT_MOST);
-        int childHeightSpec = MeasureSpec.makeMeasureSpec(requestedHeight, MeasureSpec.AT_MOST);
-
-        int availableHeight = requestedHeight;
-        int largestWidth = 0;
-        int itemCount = mAdapter.getItemCount();
-        int currentAdapterPosition = mStartPosition;
-
-        mBoundViews = 0;
-
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, String.format("onMeasure(); requestedWidth: %d, requestedHeight: %d, "
-                    + "availableHeight: %d", requestedWidth, requestedHeight, availableHeight));
-        }
-
-        int availableHeightDownwards = availableHeight - mTopOffset;
-
-        // Starting from the top offset, measure the views that can fit downwards.
-        while (availableHeightDownwards >= 0) {
-            View childView = getChildView(mBoundViews);
-
-            mAdapter.bindView(childView, currentAdapterPosition,
-                    currentAdapterPosition == mStartPosition);
-            mBoundViews++;
-
-            // Ensure that only the first view has full alpha.
-            if (currentAdapterPosition == mStartPosition) {
-                childView.setAlpha(FIRST_VIEW_ALPHA);
-            } else {
-                childView.setAlpha(DEFAULT_VIEW_ALPHA);
-            }
-
-            childView.measure(childWidthSpec, childHeightSpec);
-
-            largestWidth = Math.max(largestWidth, childView.getMeasuredWidth());
-            availableHeightDownwards -= childView.getMeasuredHeight();
-
-            // Wrap the current adapter position if necessary.
-            if (++currentAdapterPosition == itemCount) {
-                currentAdapterPosition = 0;
-            }
-
-            if (Log.isLoggable(TAG, Log.VERBOSE)) {
-                Log.v(TAG, "Measuring views downwards; current position: "
-                        + currentAdapterPosition);
-            }
-
-            // Break if there are no more views to bind.
-            if (mBoundViews == itemCount) {
-                break;
-            }
-        }
-
-        int availableHeightUpwards = mTopOffset;
-        currentAdapterPosition = mStartPosition;
-
-        // Starting from the top offset, measure the views that can fit upwards.
-        while (availableHeightUpwards >= 0) {
-            // Wrap the current adapter position if necessary.
-            if (--currentAdapterPosition < 0) {
-                currentAdapterPosition = itemCount - 1;
-            }
-
-            if (Log.isLoggable(TAG, Log.VERBOSE)) {
-                Log.v(TAG, "Measuring views upwards; current position: "
-                        + currentAdapterPosition);
-            }
-
-            View childView = getChildView(mBoundViews);
-
-            mAdapter.bindView(childView, currentAdapterPosition,
-                    currentAdapterPosition == mStartPosition);
-            mBoundViews++;
-
-            // We know that the first view will be measured in the "downwards" pass, so all these
-            // views can have DEFAULT_VIEW_ALPHA.
-            childView.setAlpha(DEFAULT_VIEW_ALPHA);
-            childView.measure(childWidthSpec, childHeightSpec);
-
-            largestWidth = Math.max(largestWidth, childView.getMeasuredWidth());
-            availableHeightUpwards -= childView.getMeasuredHeight();
-
-            // Break if there are no more views to bind.
-            if (mBoundViews == itemCount) {
-                break;
-            }
-        }
-
-        int width = widthMode == MeasureSpec.EXACTLY
-                ? requestedWidth
-                : Math.min(largestWidth, requestedWidth);
-
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, String.format("Measure finished. Largest width is %s; "
-                    + "setting final width as %s.", largestWidth, width));
-        }
-
-        setMeasuredDimension(width, requestedHeight);
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int l, int t, int r, int b) {
-        int height = b - t;
-        int width = r - l;
-
-        int top = mTopOffset;
-        int viewsLaidOut = 0;
-        int currentPosition = 0;
-        LayoutParams layoutParams = getLayoutParams();
-
-        // Double check that the item count has not changed since the views have been bound.
-        if (mBoundViews > mAdapter.getItemCount()) {
-            return;
-        }
-
-        // Start laying out the views from the first position downwards.
-        for (; viewsLaidOut < mBoundViews; viewsLaidOut++) {
-            View childView = mScrapViews.get(currentPosition);
-            addViewInLayout(childView, -1, layoutParams);
-            int measuredHeight = childView.getMeasuredHeight();
-
-            childView.layout(width - childView.getMeasuredWidth(), top, width,
-                    top + measuredHeight);
-
-            top += mItemMargin + measuredHeight;
-
-            // Wrap the current position if necessary.
-            if (++currentPosition >= mBoundViews) {
-                currentPosition = 0;
-            }
-
-            // Check if there is still space to fit another view. If not, then stop layout.
-            if (top >= height) {
-                // Increase the number of views laid out by 1 since this usually will happen at the
-                // end of the loop, but we are breaking out of it.
-                viewsLaidOut++;
-                break;
-            }
-        }
-
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, String.format("onLayout(). First pass laid out %s views", viewsLaidOut));
-        }
-
-        // Reset the top position to the first position's top and the starting position.
-        top = mTopOffset;
-        currentPosition = 0;
-
-        // Now, if there are any views remaining, back-fill the space above the first position.
-        for (; viewsLaidOut < mBoundViews; viewsLaidOut++) {
-            // Wrap the current position if necessary. Since this is a back-fill, we will subtract
-            // from the current position.
-            if (--currentPosition < 0) {
-                currentPosition = mBoundViews - 1;
-            }
-
-            View childView = mScrapViews.get(currentPosition);
-            addViewInLayout(childView, -1, layoutParams);
-            int measuredHeight = childView.getMeasuredHeight();
-
-            top -= measuredHeight + mItemMargin;
-
-            childView.layout(width - childView.getMeasuredWidth(), top, width,
-                    top + measuredHeight);
-
-            // Check if there is still space to fit another view.
-            if (top <= 0) {
-                // Although this value is not technically needed, increasing its value so that the
-                // debug statement will print out the correct value.
-                viewsLaidOut++;
-                break;
-            }
-        }
-
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, String.format("onLayout(). Second pass total laid out %s views",
-                    viewsLaidOut));
-        }
-    }
-
-    /**
-     * Returns the {@link View} that should be drawn at the given position.
-     */
-    private View getChildView(int position) {
-        View childView;
-
-        // Check if there is already a View in the scrap pile of Views that can be used. Otherwise,
-        // create a new View and add it to the scrap.
-        if (mScrapViews.size() > position) {
-            childView = mScrapViews.get(position);
-        } else {
-            childView = mAdapter.createView(this /* parent */);
-            mScrapViews.add(childView);
-        }
-
-        return childView;
-    }
-
-    /**
-     * Returns the default height that the {@link CarouselView} will take up. This will be the
-     * height of the current screen.
-     */
-    private int getDefaultHeight() {
-        return getDisplayMetrics(getContext()).heightPixels;
-    }
-
-    /**
-     * Returns the default width that the {@link CarouselView} will take up. This will be the width
-     * of the current screen.
-     */
-    private int getDefaultWidth() {
-        return getDisplayMetrics(getContext()).widthPixels;
-    }
-
-    /**
-     * Returns a {@link DisplayMetrics} object that can be used to query the height and width of the
-     * current device's screen.
-     */
-    private static DisplayMetrics getDisplayMetrics(Context context) {
-        WindowManager windowManager = (WindowManager) context.getSystemService(
-                Context.WINDOW_SERVICE);
-        DisplayMetrics displayMetrics = new DisplayMetrics();
-        windowManager.getDefaultDisplay().getMetrics(displayMetrics);
-        return displayMetrics;
-    }
-
-    /**
-     * A data set adapter for the {@link CarouselView} that is responsible for providing the views
-     * to be displayed as well as binding data on those views.
-     */
-    public static abstract class Adapter extends Observable<CarouselView> {
-        /**
-         * Returns a View to be displayed. The views returned should all be the same.
-         *
-         * @param parent The {@link CarouselView} that the views will be attached to.
-         * @return A non-{@code null} View.
-         */
-        public abstract View createView(ViewGroup parent);
-
-        /**
-         * Binds the given View with data. The View passed to this method will be the same View
-         * returned by {@link #createView(ViewGroup)}.
-         *
-         * @param view The View to bind with data.
-         * @param position The position of the View in the carousel.
-         * @param isFirstView {@code true} if the view being bound is the first view in the
-         *                    carousel.
-         */
-        public abstract void bindView(View view, int position, boolean isFirstView);
-
-        /**
-         * Returns the total number of unique items that will be displayed in the
-         * {@link CarouselView}.
-         */
-        public abstract int getItemCount();
-
-        /**
-         * Notify the {@link CarouselView} that the data set has changed. This will cause the
-         * {@link CarouselView} to re-layout itself.
-         */
-        public final void notifyDataSetChanged() {
-            if (mObservers.size() > 0) {
-                for (CarouselView carouselView : mObservers) {
-                    carouselView.requestLayout();
-                }
-            }
-        }
-    }
-}
diff --git a/src/com/android/car/radio/FragmentWithFade.java b/src/com/android/car/radio/FragmentWithFade.java
index caf7c66..956046e 100644
--- a/src/com/android/car/radio/FragmentWithFade.java
+++ b/src/com/android/car/radio/FragmentWithFade.java
@@ -33,4 +33,3 @@
      */
     void fadeInContent();
 }
-
diff --git a/src/com/android/car/radio/ManualTunerController.java b/src/com/android/car/radio/ManualTunerController.java
index 8f15428..1ca5171 100644
--- a/src/com/android/car/radio/ManualTunerController.java
+++ b/src/com/android/car/radio/ManualTunerController.java
@@ -15,14 +15,15 @@
  */
 package com.android.car.radio;
 
+import android.annotation.NonNull;
 import android.content.Context;
+import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
-import android.support.annotation.NonNull;
 import android.view.View;
 import android.widget.Button;
 import android.widget.TextView;
 
-import com.android.car.radio.service.RadioStation;
+import com.android.car.broadcastradio.support.platform.ProgramSelectorExt;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -109,10 +110,10 @@
          * Called when the done button has been clicked with the given station that the user has
          * selected.
          */
-        void onDone(RadioStation station);
+        void onDone(ProgramSelector sel);
     }
 
-    ManualTunerController(Context context, View container, int currentRadioBand) {
+    public ManualTunerController(Context context, View container, int currentRadioBand) {
         mChannelView = container.findViewById(R.id.manual_tuner_channel);
 
         // Default to FM band.
@@ -157,27 +158,13 @@
         mDoneButton = container.findViewById(R.id.manual_tuner_done_button);
 
         View backButton = container.findViewById(R.id.exit_manual_tuner_button);
-        backButton.setOnClickListener(v -> {
-            if (mManualTunerClickListener != null) {
-                mManualTunerClickListener.onBack();
-            }
-        });
-
-        amBandButton.setOnClickListener(v -> {
-            mCurrentRadioBand = RadioManager.BAND_AM;
-            mChannelValidator = mAmChannelValidator;
-            amBandButton.setIsBandSelected(true);
-            fmBandButton.setIsBandSelected(false);
-            resetChannel();
-        });
-
-        fmBandButton.setOnClickListener(v -> {
-            mCurrentRadioBand = RadioManager.BAND_FM;
-            mChannelValidator = mFmChannelValidator;
-            amBandButton.setIsBandSelected(false);
-            fmBandButton.setIsBandSelected(true);
-            resetChannel();
-        });
+        if (backButton != null) {
+            backButton.setOnClickListener(v -> {
+                if (mManualTunerClickListener != null) {
+                    mManualTunerClickListener.onBack();
+                }
+            });
+        }
 
         mDoneButton.setOnClickListener(v -> {
             if (mManualTunerClickListener == null) {
@@ -185,20 +172,49 @@
             }
 
             int channelFrequency = mChannelValidator.convertToHz(mCurrentChannel.toString());
-            RadioStation station = new RadioStation(channelFrequency, 0 /* subChannelNumber */,
-                    mCurrentRadioBand, null /* rds */);
-
-            mManualTunerClickListener.onDone(station);
+            mManualTunerClickListener.onDone(
+                    ProgramSelectorExt.createAmFmSelector(channelFrequency));
         });
 
-        if (mCurrentRadioBand == RadioManager.BAND_AM) {
+        if (amBandButton != null) {
+            amBandButton.setOnClickListener(v -> {
+                mCurrentRadioBand = RadioManager.BAND_AM;
+                mChannelValidator = mAmChannelValidator;
+                amBandButton.setIsBandSelected(true);
+                fmBandButton.setIsBandSelected(false);
+                resetChannel();
+            });
+        }
+        if (fmBandButton != null) {
+            fmBandButton.setOnClickListener(v -> {
+                mCurrentRadioBand = RadioManager.BAND_FM;
+                mChannelValidator = mFmChannelValidator;
+                amBandButton.setIsBandSelected(false);
+                fmBandButton.setIsBandSelected(true);
+                resetChannel();
+            });
+        }
+        if (mCurrentRadioBand == RadioManager.BAND_AM && amBandButton != null) {
             amBandButton.setIsBandSelected(true);
-        } else {
+        } else if (fmBandButton != null) {
             fmBandButton.setIsBandSelected(true);
         }
     }
 
     /**
+     * Refreshes tuner key state with new radio band, if changed without using AM/FM band buttons
+     */
+    public void updateCurrentRadioBand(int band) {
+        mCurrentRadioBand = band;
+        if (band == RadioManager.BAND_FM) {
+            mChannelValidator = mFmChannelValidator;
+        } else {
+            mChannelValidator = mAmChannelValidator;
+        }
+        resetChannel();
+    }
+
+    /**
      * Sets up the click listeners and tags for the manual tuner buttons.
      */
     private void initializeManualTunerButtons(View container) {
@@ -260,7 +276,7 @@
      * Sets the given {@link ManualTunerClickListener} to be notified when the done button of the manual
      * tuner has been clicked.
      */
-    void setDoneButtonListener(ManualTunerClickListener listener) {
+    public void setDoneButtonListener(ManualTunerClickListener listener) {
         mManualTunerClickListener = listener;
     }
 
diff --git a/src/com/android/car/radio/ManualTunerFragment.java b/src/com/android/car/radio/ManualTunerFragment.java
index 3ccf7a3..6c58eb2 100644
--- a/src/com/android/car/radio/ManualTunerFragment.java
+++ b/src/com/android/car/radio/ManualTunerFragment.java
@@ -15,16 +15,15 @@
  */
 package com.android.car.radio;
 
+import android.annotation.Nullable;
+import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
 import android.os.Bundle;
-import android.support.annotation.Nullable;
 import android.support.v4.app.Fragment;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 
-import com.android.car.radio.service.RadioStation;
-
 /**
  * A fragment that allows the user to manually input a radio station to tune to.
  */
@@ -46,10 +45,10 @@
          * user exits the manual tuner without a station being selected, then {@code null} will
          * be passed to this method.
          *
-         * @param station The {@link RadioStation} that was selected or {@code null} if the user
+         * @param station The {@link ProgramSelector} that was selected or {@code null} if the user
          *                exits before selecting one.
          */
-        void onStationSelected(@Nullable RadioStation station);
+        void onStationSelected(@Nullable ProgramSelector sel);
     }
 
     @Override
@@ -80,9 +79,9 @@
     }
 
     @Override
-    public void onDone(RadioStation station) {
+    public void onDone(ProgramSelector sel) {
         if (mListener != null) {
-            mListener.onStationSelected(station);
+            mListener.onStationSelected(sel);
         }
     }
 
diff --git a/src/com/android/car/radio/PlayPauseButton.java b/src/com/android/car/radio/PlayPauseButton.java
index 878322d..0e1f82d 100644
--- a/src/com/android/car/radio/PlayPauseButton.java
+++ b/src/com/android/car/radio/PlayPauseButton.java
@@ -22,8 +22,6 @@
 import android.util.Log;
 import android.widget.ImageView;
 
-import com.android.car.apps.common.FabDrawable;
-
 /**
  * An {@link ImageView} that renders a play/pause button like a floating action button.
  */
@@ -37,11 +35,6 @@
 
     public PlayPauseButton(Context context, AttributeSet attrs) {
         super(context, attrs);
-
-        FabDrawable fabDrawable = new FabDrawable(context);
-        fabDrawable.setFabAndStrokeColor(
-                context.getColor(R.color.car_radio_accent_color));
-        setBackground(fabDrawable);
     }
 
     /**
diff --git a/src/com/android/car/radio/PreScannedChannelLoader.java b/src/com/android/car/radio/PreScannedChannelLoader.java
deleted file mode 100644
index 6d495f8..0000000
--- a/src/com/android/car/radio/PreScannedChannelLoader.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.car.radio;
-
-import android.content.AsyncTaskLoader;
-import android.content.Context;
-import android.support.annotation.NonNull;
-import com.android.car.radio.service.RadioStation;
-
-import java.util.Collections;
-import java.util.List;
-
-/**
- * An {@link AsyncTaskLoader} that will load all the pre-scanned radio stations for a given band.
- */
-public class PreScannedChannelLoader extends AsyncTaskLoader<List<RadioStation>> {
-    private final static int INVALID_RADIO_BAND = -1;
-
-    private final RadioStorage mRadioStorage;
-    private int mCurrentRadioBand = INVALID_RADIO_BAND;
-
-    public PreScannedChannelLoader(Context context) {
-        super(context);
-        mRadioStorage = RadioStorage.getInstance(context);
-    }
-
-    /**
-     * Sets the radio band that this loader should load the pre-scanned stations for.
-     *
-     * @param radioBand One of the band values in {@link android.hardware.radio.RadioManager}. For
-     *                  example, {@link android.hardware.radio.RadioManager#BAND_FM}.
-     */
-    public void setCurrentRadioBand(int radioBand) {
-        mCurrentRadioBand = radioBand;
-    }
-
-    /**
-     * Returns a list of {@link RadioStation}s representing the pre-scanned channels for the band
-     * specified by {@link #INVALID_RADIO_BAND}. If no stations exist for the given band, then an
-     * empty list is returned.
-     */
-    @NonNull
-    @Override
-    public List<RadioStation> loadInBackground() {
-        if (mCurrentRadioBand == INVALID_RADIO_BAND) {
-            return Collections.emptyList();
-        }
-
-        return mRadioStorage.getPreScannedStationsForBand(mCurrentRadioBand);
-    }
-}
diff --git a/src/com/android/car/radio/PrescannedRadioStationAdapter.java b/src/com/android/car/radio/PrescannedRadioStationAdapter.java
deleted file mode 100644
index c75d325..0000000
--- a/src/com/android/car/radio/PrescannedRadioStationAdapter.java
+++ /dev/null
@@ -1,187 +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.car.radio;
-
-import android.support.annotation.Nullable;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import com.android.car.radio.service.RadioStation;
-
-import java.util.List;
-
-/**
- * A {@link com.android.car.radio.CarouselView.Adapter} that supplies the views to be displayed
- * from a list of {@link RadioStation}s.
- */
-public class PrescannedRadioStationAdapter extends CarouselView.Adapter {
-    private static final String TAG = "PreScanAdapter";
-
-    private List<RadioStation> mStations;
-    private int mCurrentPosition;
-
-    /**
-     * Sets the {@link RadioStation}s that will be used to bind to the views to be displayed.
-     */
-    public void setStations(List<RadioStation> stations) {
-        mStations = stations;
-        notifyDataSetChanged();
-    }
-
-    /**
-     * Sets the the station within the list passed to {@link #setStations(List)} that should be
-     * the first station displayed. A station is identified by the channel number and band.
-     *
-     * @return The index within the list of stations passed to {@link #setStations(List)} that the
-     * starting station can be found at.
-     */
-    public int setStartingStation(int channelNumber, int band) {
-        getIndexOrInsertForStation(channelNumber, band);
-        return mCurrentPosition;
-    }
-
-    /**
-     * Returns the station that is currently the first station to be displayed. This value can be
-     * different from the value returned by {@link #setStartingStation(int, int)} if either
-     * {@link #getPrevStation()} or {@link #getNextStation()} have been called.
-     */
-    public int getCurrentPosition() {
-        return mCurrentPosition;
-    }
-
-    /**
-     * Returns the previous station in the list based off the value returned by
-     * {@link #getCurrentPosition()}. After calling this method, the current position returned by
-     * that method will be the index of the {@link RadioStation} returned by this method.
-     */
-    @Nullable
-    public RadioStation getPrevStation() {
-        if (mStations == null) {
-            return null;
-        }
-
-        if (--mCurrentPosition < 0) {
-            mCurrentPosition = mStations.size() - 1;
-        }
-
-        return mStations.get(mCurrentPosition);
-    }
-
-    /**
-     * Returns the next station in the list based off the value returned by
-     * {@link #getCurrentPosition()}. After calling this method, the current position returned by
-     * that method will be the index of the {@link RadioStation} returned by this method.
-     */
-    @Nullable
-    public RadioStation getNextStation() {
-        if (mStations == null) {
-            return null;
-        }
-
-        if (++mCurrentPosition >= mStations.size()) {
-            mCurrentPosition = 0;
-        }
-
-        return mStations.get(mCurrentPosition);
-    }
-
-    /**
-     * Returns the index in the list set in {@link #setStations(List)} that corresponds to the
-     * given channel number and band. If the given combination does not exist within the list, then
-     * it will be inserted in ascending order.
-     *
-     * @return An index into the list or -1 if no list of stations has been set.
-     */
-    public int getIndexOrInsertForStation(int channelNumber, int band) {
-        if (mStations == null) {
-            mCurrentPosition = -1;
-            return -1;
-        }
-
-        int indexToInsert = 0;
-
-        for (int i = 0, size = mStations.size(); i < size; i++) {
-            RadioStation station = mStations.get(i);
-
-            if (channelNumber >= station.getChannelNumber()) {
-                // Need to add 1 to the index because the channel should be inserted after it.
-                indexToInsert = i + 1;
-            }
-
-            if (station.getChannelNumber() == channelNumber && station.getRadioBand() == band) {
-                mCurrentPosition = i;
-                return i;
-            }
-        }
-
-        // If this path is reached, that means an exact match for the station was not found in
-        // the given list. Instead, insert the station into the list and return that index.
-        RadioStation stationToInsert = new RadioStation(channelNumber, 0 /* subChannel */,
-                band, null /* rds */);
-        mStations.add(indexToInsert, stationToInsert);
-        notifyDataSetChanged();
-
-        mCurrentPosition = indexToInsert;
-        return indexToInsert;
-    }
-
-    @Override
-    public View createView(ViewGroup parent) {
-        return LayoutInflater.from(parent.getContext()).inflate(R.layout.radio_channel, parent,
-                false);
-    }
-
-    @Override
-    public void bindView(View view, int position, boolean isFirstView) {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "bindView(); position: " + position + "; isFirstView: " + isFirstView);
-        }
-
-        if (mStations == null || position < 0 || position >= mStations.size()) {
-            return;
-        }
-
-        TextView radioChannel = view.findViewById(R.id.radio_list_station_channel);
-        TextView radioBandView = view.findViewById(R.id.radio_list_station_band);
-
-        RadioStation station = mStations.get(position);
-
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "binding station: " + station);
-        }
-
-        int radioBand = station.getRadioBand();
-
-        radioChannel.setText(RadioChannelFormatter.formatRadioChannel(radioBand,
-                station.getChannelNumber()));
-
-        if (isFirstView) {
-            radioBandView.setText(RadioChannelFormatter.formatRadioBand(view.getContext(),
-                    radioBand));
-        } else {
-            radioBandView.setText(null);
-        }
-    }
-
-    @Override
-    public int getItemCount() {
-        return mStations == null ? 0 : mStations.size();
-    }
-}
diff --git a/src/com/android/car/radio/PresetListScrollListener.java b/src/com/android/car/radio/PresetListScrollListener.java
index 3ec9f36..527fda8 100644
--- a/src/com/android/car/radio/PresetListScrollListener.java
+++ b/src/com/android/car/radio/PresetListScrollListener.java
@@ -18,9 +18,11 @@
 
 import android.animation.ValueAnimator;
 import android.content.Context;
+import android.support.v7.widget.LinearLayoutManager;
 import android.support.v7.widget.RecyclerView;
 import android.view.View;
-import com.android.car.view.PagedListView;
+
+import androidx.car.widget.PagedListView;
 
 /**
  * Listener on the preset list that will add elevation on the container holding the current
@@ -57,7 +59,12 @@
             return;
         }
 
-        if (mPresetList.getLayoutManager().isAtTop()) {
+        // The default LayoutManager for PagedListView is a LinearLayoutManager. Radio does
+        // not change this.
+        LinearLayoutManager layoutManager =
+                (LinearLayoutManager) recyclerView.getLayoutManager();
+
+        if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
             // Animate the removal of the elevation so that it's not jarring.
             mRemoveElevationAnimator.start();
         } else {
diff --git a/src/com/android/car/radio/PresetsAdapter.java b/src/com/android/car/radio/PresetsAdapter.java
index 6b0a0ce..3c1de48 100644
--- a/src/com/android/car/radio/PresetsAdapter.java
+++ b/src/com/android/car/radio/PresetsAdapter.java
@@ -16,30 +16,36 @@
 
 package com.android.car.radio;
 
+import android.annotation.Nullable;
+import android.hardware.radio.ProgramSelector;
 import android.support.v7.widget.RecyclerView;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import com.android.car.radio.service.RadioStation;
-import com.android.car.view.PagedListView;
+
+import androidx.car.widget.PagedListView;
+
+import com.android.car.broadcastradio.support.Program;
 
 import java.util.List;
+import java.util.Objects;
 
 /**
  * Adapter that will display a list of radio stations that represent the user's presets.
  */
 public class PresetsAdapter extends RecyclerView.Adapter<PresetsViewHolder>
-        implements PresetsViewHolder.OnPresetClickListener, PagedListView.ItemCap {
+        implements PagedListView.ItemCap {
     private static final String TAG = "Em.PresetsAdapter";
 
     // Only one type of view in this adapter.
     private static final int PRESETS_VIEW_TYPE = 0;
 
-    private RadioStation mActiveRadioStation;
+    private Program mActiveProgram;
 
-    private List<RadioStation> mPresets;
+    private List<Program> mPresets;
     private OnPresetItemClickListener mPresetClickListener;
+    private OnPresetItemFavoriteListener mPresetFavoriteListener;
 
     /**
      * Interface for a listener that will be notified when an item in the presets list has been
@@ -49,22 +55,44 @@
         /**
          * Method called when an item in the preset list has been clicked.
          *
-         * @param radioStation The {@link RadioStation} corresponding to the clicked preset.
+         * @param selector The {@link ProgramSelector} corresponding to the clicked preset.
          */
-        void onPresetItemClicked(RadioStation radioStation);
+        void onPresetItemClicked(ProgramSelector selector);
+    }
+
+    /**
+     * Interface for a listener that will be notified when a favorite in the presets list has been
+     * toggled.
+     */
+    public interface OnPresetItemFavoriteListener {
+
+        /**
+         * Method called when an item's favorite status has been toggled
+         *
+         * @param program The {@link Program} corresponding to the clicked preset.
+         * @param saveAsFavorite Whether the program should be saved or removed as a favorite.
+         */
+        void onPresetItemFavoriteChanged(Program program, boolean saveAsFavorite);
     }
 
     /**
      * Set a listener to be notified whenever a preset card is pressed.
      */
-    public void setOnPresetItemClickListener(OnPresetItemClickListener listener) {
-        mPresetClickListener = listener;
+    public void setOnPresetItemClickListener(@Nullable OnPresetItemClickListener listener) {
+        mPresetClickListener = Objects.requireNonNull(listener);
+    }
+
+    /**
+     * Set a listener to be notified whenever a preset favorite is changed.
+     */
+    public void setOnPresetItemFavoriteListener(@Nullable OnPresetItemFavoriteListener listener) {
+        mPresetFavoriteListener = listener;
     }
 
     /**
      * Sets the given list as the list of presets to display.
      */
-    public void setPresets(List<RadioStation> presets) {
+    public void setPresets(List<Program> presets) {
         mPresets = presets;
         notifyDataSetChanged();
     }
@@ -74,8 +102,8 @@
      * this adapter. This will cause that station to be highlighted in the list. If the station
      * passed to this method does not match any of the presets, then none will be highlighted.
      */
-    public void setActiveRadioStation(RadioStation station) {
-        mActiveRadioStation = station;
+    public void setActiveProgram(Program program) {
+        mActiveProgram = program;
         notifyDataSetChanged();
     }
 
@@ -84,27 +112,19 @@
         View view = LayoutInflater.from(parent.getContext())
                 .inflate(R.layout.radio_preset_stream_card, parent, false);
 
-        return new PresetsViewHolder(view, this /* listener */);
+        return new PresetsViewHolder(
+                view, this::handlePresetClicked, this::handlePresetFavoriteChanged);
     }
 
     @Override
     public void onBindViewHolder(PresetsViewHolder holder, int position) {
-        RadioStation station = mPresets.get(position);
-        boolean isActiveStation = station.equals(mActiveRadioStation);
-
-        holder.bindPreset(station, isActiveStation, getItemCount());
-    }
-
-    @Override
-    public void onPresetClicked(int position) {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, String.format("onPresetClicked(); item count: %d; position: %d",
-                    getItemCount(), position));
+        if (getPresetCount() == 0) {
+            holder.bindPreset(mActiveProgram, true, getItemCount(), false);
+            return;
         }
-
-        if (mPresetClickListener != null && getItemCount() > position) {
-            mPresetClickListener.onPresetItemClicked(mPresets.get(position));
-        }
+        Program station = mPresets.get(position);
+        boolean isActiveStation = station.getSelector().equals(mActiveProgram.getSelector());
+        holder.bindPreset(station, isActiveStation, getItemCount(), true);
     }
 
     @Override
@@ -114,7 +134,12 @@
 
     @Override
     public int getItemCount() {
-        return mPresets == null ? 0 : mPresets.size();
+        int numPresets = getPresetCount();
+        return (numPresets == 0) ? 1 : numPresets;
+    }
+
+    private int getPresetCount() {
+        return (mPresets == null) ? 0 : mPresets.size();
     }
 
     @Override
@@ -122,4 +147,29 @@
         // No-op. A PagedListView needs the ItemCap interface to be implemented. However, the
         // list of presets should not be limited.
     }
+
+    private void handlePresetClicked(int position) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, String.format("onPresetClicked(); item count: %d; position: %d",
+                    getItemCount(), position));
+        }
+        if (mPresetClickListener != null && getItemCount() > position) {
+            if (getPresetCount() == 0) {
+                mPresetClickListener.onPresetItemClicked(mActiveProgram.getSelector());
+                return;
+            }
+            mPresetClickListener.onPresetItemClicked(mPresets.get(position).getSelector());
+        }
+    }
+
+    private void handlePresetFavoriteChanged (int position, boolean saveAsFavorite) {
+        if (mPresetFavoriteListener != null && getItemCount() > position) {
+            if (getPresetCount() == 0) {
+                mPresetFavoriteListener.onPresetItemFavoriteChanged(mActiveProgram, saveAsFavorite);
+                return;
+            }
+            mPresetFavoriteListener.onPresetItemFavoriteChanged(
+                    mPresets.get(position), saveAsFavorite);
+        }
+    }
 }
diff --git a/src/com/android/car/radio/PresetsViewHolder.java b/src/com/android/car/radio/PresetsViewHolder.java
index 84e738b..a34be3f 100644
--- a/src/com/android/car/radio/PresetsViewHolder.java
+++ b/src/com/android/car/radio/PresetsViewHolder.java
@@ -16,18 +16,24 @@
 
 package com.android.car.radio;
 
+import android.annotation.NonNull;
 import android.content.Context;
 import android.graphics.drawable.GradientDrawable;
-import android.support.annotation.NonNull;
+import android.hardware.radio.ProgramSelector;
 import android.support.v7.widget.RecyclerView;
-import android.text.TextUtils;
 import android.util.Log;
 import android.view.View;
+import android.widget.ImageButton;
 import android.widget.TextView;
-import com.android.car.radio.service.RadioStation;
+
+import com.android.car.broadcastradio.support.Program;
+import com.android.car.broadcastradio.support.platform.ProgramSelectorExt;
+import com.android.car.view.CardListBackgroundResolver;
+
+import java.util.Objects;
 
 /**
- * A {@link RecyclerView.ViewHolder} that can bind a {@link RadioStation} to the layout
+ * A {@link RecyclerView.ViewHolder} that can bind a {@link Program} to the layout
  * {@code R.layout.radio_preset_item}.
  */
 public class PresetsViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
@@ -36,13 +42,14 @@
     private final RadioChannelColorMapper mColorMapper;
 
     private final OnPresetClickListener mPresetClickListener;
+    private final OnPresetFavoriteListener mPresetFavoriteListener;
 
     private final Context mContext;
     private final View mPresetsCard;
     private GradientDrawable mPresetItemChannelBg;
     private final TextView mPresetItemChannel;
     private final TextView mPresetItemMetadata;
-    private final View mEqualizer;
+    private ImageButton mPresetButton;
 
     /**
      * Interface for a listener when the View held by this ViewHolder has been clicked.
@@ -57,10 +64,25 @@
         void onPresetClicked(int position);
     }
 
+
+    /**
+     * Interface for a listener that will be notified when a favorite in the presets list has been
+     * toggled.
+     */
+    public interface OnPresetFavoriteListener {
+
+        /**
+         * Method called when an item's favorite status has been toggled
+         */
+        void onPresetFavoriteChanged(int position, boolean saveAsFavorite);
+    }
+
+
     /**
      * @param presetsView A view that contains the layout {@code R.layout.radio_preset_item}.
      */
-    public PresetsViewHolder(@NonNull View presetsView, @NonNull OnPresetClickListener listener) {
+    public PresetsViewHolder(@NonNull View presetsView, @NonNull OnPresetClickListener listener,
+            @NonNull OnPresetFavoriteListener favoriteListener) {
         super(presetsView);
 
         mContext = presetsView.getContext();
@@ -69,13 +91,15 @@
         mPresetsCard.setOnClickListener(this);
 
         mColorMapper = RadioChannelColorMapper.getInstance(mContext);
-        mPresetClickListener = listener;
+        mPresetClickListener = Objects.requireNonNull(listener);
+        mPresetFavoriteListener = Objects.requireNonNull(favoriteListener);
 
         mPresetItemChannel = presetsView.findViewById(R.id.preset_station_channel);
         mPresetItemMetadata = presetsView.findViewById(R.id.preset_item_metadata);
-        mEqualizer = presetsView.findViewById(R.id.preset_equalizer);
+        mPresetButton = presetsView.findViewById(R.id.preset_button);
 
-        mPresetItemChannelBg = (GradientDrawable) mPresetItemChannel.getBackground();
+        mPresetItemChannelBg = (GradientDrawable)
+                presetsView.findViewById(R.id.preset_station_background).getBackground();
     }
 
     @Override
@@ -88,62 +112,58 @@
     }
 
     /**
-     * Binds the given {@link RadioStation} to this View within this ViewHolder.
+     * Binds the given {@link Program} to this View within this ViewHolder.
      */
-    public void bindPreset(RadioStation preset, boolean isActiveStation, int itemCount) {
+    public void bindPreset(Program program, boolean isActiveStation, int itemCount,
+            boolean isFavorite) {
         // If the preset is null, clear any existing text.
-        if (preset == null) {
+        if (program == null) {
             mPresetItemChannel.setText(null);
             mPresetItemMetadata.setText(null);
             mPresetItemChannelBg.setColor(mColorMapper.getDefaultColor());
             return;
         }
 
-        setPresetCardBackground(itemCount);
+        ProgramSelector sel = program.getSelector();
 
-        String channelNumber = RadioChannelFormatter.formatRadioChannel(preset.getRadioBand(),
-                preset.getChannelNumber());
+        CardListBackgroundResolver.setBackground(mPresetsCard, getAdapterPosition(), itemCount);
 
-        mPresetItemChannel.setText(channelNumber);
+        mPresetItemChannel.setText(ProgramSelectorExt.getDisplayName(
+                sel, ProgramSelectorExt.NAME_NO_MODULATION));
+        if (isActiveStation) {
+            mPresetItemChannel.setCompoundDrawablesRelativeWithIntrinsicBounds(
+                    R.drawable.ic_equalizer, 0, 0, 0);
+        } else {
+            mPresetItemChannel.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0);
+        }
 
-        mEqualizer.setVisibility(isActiveStation ? View.VISIBLE : View.GONE);
+        mPresetItemChannelBg.setColor(mColorMapper.getColorForProgram(sel));
 
-        mPresetItemChannelBg.setColor(mColorMapper.getColorForStation(preset));
-
-        String metadata = preset.getRds() == null ? null : preset.getRds().getProgramService();
-
-        if (TextUtils.isEmpty(metadata)) {
+        String programName = program.getName();
+        if (programName.isEmpty()) {
             // If there is no metadata text, then use text to indicate the favorite number to the
             // user so that list does not appear empty.
             mPresetItemMetadata.setText(mContext.getString(
                     R.string.radio_default_preset_metadata_text, getAdapterPosition() + 1));
         } else {
-            mPresetItemMetadata.setText(metadata.trim());
+            mPresetItemMetadata.setText(programName);
         }
+        setFavoriteButtonFilled(isFavorite);
+        mPresetButton.setOnClickListener(v -> {
+            boolean favoriteToggleOn =
+                    ((Integer) mPresetButton.getTag() == R.drawable.ic_star_empty);
+            setFavoriteButtonFilled(favoriteToggleOn);
+            mPresetFavoriteListener.onPresetFavoriteChanged(getAdapterPosition(), favoriteToggleOn);
+        });
     }
 
-    /**
-     * Sets the appropriate background on the card containing the preset information. The cards
-     * need to have rounded corners depending on its position in the list and the number of items
-     * in the list.
-     */
-    private void setPresetCardBackground(int itemCount) {
-        int position = getAdapterPosition();
-
-        // Correctly set the background for each card. Only the top and last card should
-        // have rounded corners.
-        if (itemCount == 1) {
-            // One card - all corners are rounded
-            mPresetsCard.setBackgroundResource(R.drawable.preset_item_card_rounded_bg);
-        } else if (position == 0) {
-            // First card gets rounded top
-            mPresetsCard.setBackgroundResource(R.drawable.preset_item_card_rounded_top_bg);
-        } else if (position == itemCount - 1) {
-            // Last one has a rounded bottom
-            mPresetsCard.setBackgroundResource(R.drawable.preset_item_card_rounded_bottom_bg);
+    private void setFavoriteButtonFilled(boolean favoriteToggleOn) {
+        if (favoriteToggleOn) {
+            mPresetButton.setImageResource(R.drawable.ic_star_filled);
+            mPresetButton.setTag(R.drawable.ic_star_filled);
         } else {
-            // Middle have no rounded corners
-            mPresetsCard.setBackgroundResource(R.color.car_card);
+            mPresetButton.setImageResource(R.drawable.ic_star_empty);
+            mPresetButton.setTag(R.drawable.ic_star_empty);
         }
     }
 }
diff --git a/src/com/android/car/radio/RadioAnimationManager.java b/src/com/android/car/radio/RadioAnimationManager.java
index bf6b1ca..50efe97 100644
--- a/src/com/android/car/radio/RadioAnimationManager.java
+++ b/src/com/android/car/radio/RadioAnimationManager.java
@@ -20,10 +20,10 @@
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
+import android.annotation.NonNull;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Point;
-import android.support.annotation.NonNull;
 import android.support.v4.view.animation.FastOutSlowInInterpolator;
 import android.support.v7.widget.CardView;
 import android.view.Display;
@@ -32,7 +32,7 @@
 import android.view.ViewTreeObserver;
 import android.view.WindowManager;
 
-import com.android.car.stream.ui.ColumnCalculator;
+import androidx.car.utils.ColumnCalculator;
 
 /**
  * A animation manager that is responsible for the start and exiting animation for the
@@ -91,15 +91,16 @@
         mScreenWidth = size.x;
 
         Resources res = mContext.getResources();
-        mCardColumnSpan = res.getInteger(R.integer.stream_card_default_column_span);
+        mCardColumnSpan = res.getInteger(R.integer.column_card_default_column_span);
         mCornerRadius = res.getDimensionPixelSize(R.dimen.car_preset_item_radius);
-        mActionPanelHeight = res.getDimensionPixelSize(R.dimen.action_panel_height);
+        mActionPanelHeight = res.getDimensionPixelSize(R.dimen.car_action_bar_height);
         mPresetFinalHeight = res.getDimensionPixelSize(R.dimen.car_preset_item_height);
-        mFabSize = res.getDimensionPixelSize(R.dimen.stream_fab_size);
+        mFabSize = res.getDimensionPixelSize(R.dimen.car_radio_controls_fab_size);
         mPresetFabSize = res.getDimensionPixelSize(R.dimen.car_presets_play_button_size);
         mPresetContainerHeight = res.getDimensionPixelSize(R.dimen.car_preset_container_height);
 
         mRadioCard = container.findViewById(R.id.current_radio_station_card);
+
         mRadioCardContainer = container.findViewById(R.id.preset_current_card_container);
         mFab = container.findViewById(R.id.radio_play_button);
         mPresetFab = container.findViewById(R.id.preset_radio_play_button);
diff --git a/src/com/android/car/radio/RadioBackgroundScanner.java b/src/com/android/car/radio/RadioBackgroundScanner.java
deleted file mode 100644
index bf06314..0000000
--- a/src/com/android/car/radio/RadioBackgroundScanner.java
+++ /dev/null
@@ -1,207 +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.car.radio;
-
-import android.content.Context;
-import android.hardware.radio.RadioManager;
-import android.hardware.radio.RadioMetadata;
-import android.hardware.radio.RadioTuner;
-import android.support.annotation.Nullable;
-import android.util.Log;
-import com.android.car.radio.service.RadioRds;
-import com.android.car.radio.service.RadioStation;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A class that will scan for valid radio stations in the background and store those stations into
- * the radio database so that available stations for a particular radio band can be pre-populated
- * for the user.
- */
-public class RadioBackgroundScanner extends RadioTuner.Callback {
-    private static final String TAG = "Em.BackgroundScanner";
-    private static final int INVALID_RADIO_CHANNEL = -1;
-
-    private RadioTuner mRadioTuner;
-
-    private final RadioManager mRadioManager;
-    private final RadioManager.AmBandConfig mAmConfig;
-    private final RadioManager.FmBandConfig mFmConfig;
-    private final RadioManager.ModuleProperties mRadioModule;
-    private final RadioStorage mRadioStorage;
-
-    private int mCurrentChannel;
-    private int mCurrentBand;
-    private int mStartingChannel = INVALID_RADIO_CHANNEL;
-
-    private List<RadioStation> mScannedStations = new ArrayList<>();
-
-    public RadioBackgroundScanner(Context context, RadioManager radioManager,
-            RadioManager.AmBandConfig amConfig, RadioManager.FmBandConfig fmConfig,
-            RadioManager.ModuleProperties module) {
-        mRadioManager = radioManager;
-        mAmConfig = amConfig;
-        mFmConfig = fmConfig;
-        mRadioModule = module;
-
-        mRadioStorage = RadioStorage.getInstance(context);
-    }
-
-    /**
-     * Notify this {@link RadioBackgroundScanner} that the current radio band has changed and
-     * a new scan should start.
-     */
-    public void onRadioBandChanged(int radioBand) {
-        mStartingChannel = INVALID_RADIO_CHANNEL;
-        mCurrentBand = radioBand;
-        mScannedStations.clear();
-
-        RadioManager.BandConfig config = getRadioConfig(radioBand);
-
-        if (config == null) {
-            Log.w(TAG, "Cannot create config for radio band: " + radioBand);
-            return;
-        }
-
-        if (mRadioTuner != null) {
-            mRadioTuner.setConfiguration(config);
-        } else {
-            mRadioTuner = mRadioManager.openTuner(mRadioModule.getId(), config, true,
-                    this /* callback */, null /* handler */);
-        }
-    }
-
-    /**
-     * Returns the proper {@link android.hardware.radio.RadioManager.BandConfig} for the given
-     * radio band. {@code null} is returned if the band is not suppored.
-     */
-    @Nullable
-    private RadioManager.BandConfig getRadioConfig(int selectedRadioBand) {
-        switch (selectedRadioBand) {
-            case RadioManager.BAND_AM:
-                return mAmConfig;
-            case RadioManager.BAND_FM:
-                return mFmConfig;
-
-            // TODO: Support BAND_FM_HD and BAND_AM_HD.
-
-            default:
-                return null;
-        }
-    }
-
-    /**
-     * Replaces the given station in {@link #mScannedStations} or adds it to the end of the list if
-     * it does not exist.
-     */
-    private void addOrReplaceInScannedStations(RadioStation station) {
-        int index = mScannedStations.indexOf(station);
-
-        if (Log.isLoggable(TAG, Log.VERBOSE)) {
-            Log.v(TAG, "Storing pre-scanned station: " + station);
-        }
-
-        if (index == -1) {
-            mScannedStations.add(station);
-        } else {
-            mScannedStations.set(index, station);
-        }
-    }
-
-    @Override
-    public void onProgramInfoChanged(RadioManager.ProgramInfo info) {
-        if (Log.isLoggable(TAG, Log.VERBOSE)) {
-            Log.v(TAG, "onProgramInfoChanged(); info: " + info);
-        }
-
-        if (info == null) {
-            return;
-        }
-
-        mCurrentChannel = info.getChannel();
-
-        if (mStartingChannel == INVALID_RADIO_CHANNEL) {
-            mStartingChannel = mCurrentChannel;
-
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "Starting scan from channel: " + mStartingChannel);
-            }
-        }
-
-        // Stop scanning if we have looped back to the starting station.
-        if (mStartingChannel == mCurrentChannel) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, String.format("Looped back around to starting channel %s; storing "
-                        + "%d pre-scanned stations", mStartingChannel, mScannedStations.size()));
-            }
-
-            if (Log.isLoggable(TAG, Log.VERBOSE)) {
-                for (RadioStation station : mScannedStations) {
-                    Log.v(TAG, station.toString());
-                }
-            }
-
-            mStartingChannel = INVALID_RADIO_CHANNEL;
-
-            // Close the RadioTuner so that this class no longer receives any callbacks and store
-            // all scanned statiosn into the database.
-            mRadioTuner.close();
-            mRadioTuner = null;
-            mRadioStorage.storePreScannedStations(mCurrentBand, mScannedStations);
-            return;
-        }
-
-        RadioMetadata metadata = info.getMetadata();
-
-        // If there is no metadata, then directly store the radio information into the database.
-        // Otherwise, onMetadataChanged() can handle the storage.
-        if (metadata != null) {
-            onMetadataChanged(metadata);
-        } else {
-            RadioStation station = new RadioStation(mCurrentChannel, 0 /* subChannelNumber */,
-                    mCurrentBand, null /* rds */);
-            addOrReplaceInScannedStations(station);
-        }
-
-        // Initialize another seek to the next valid station.
-        mRadioTuner.scan(RadioTuner.DIRECTION_UP, true);
-    }
-
-    @Override
-    public void onMetadataChanged(RadioMetadata metadata) {
-        if (metadata == null) {
-            return;
-        }
-
-        String stationInfo = metadata.getString(RadioMetadata.METADATA_KEY_RDS_PS);
-        RadioRds rds = new RadioRds(stationInfo, null /* songArtist */, null /* songTitle */);
-
-        RadioStation station = new RadioStation(mCurrentChannel, 0 /* subChannelNumber */,
-                mCurrentBand, rds);
-        addOrReplaceInScannedStations(station);
-    }
-
-    @Override
-    public void onConfigurationChanged(RadioManager.BandConfig config) {
-        if (config == null) {
-            return;
-        }
-
-        mCurrentBand = config.getType();
-    }
-}
diff --git a/src/com/android/car/radio/RadioBandButton.java b/src/com/android/car/radio/RadioBandButton.java
index 0a5769a..aacc3e1 100644
--- a/src/com/android/car/radio/RadioBandButton.java
+++ b/src/com/android/car/radio/RadioBandButton.java
@@ -15,10 +15,10 @@
  */
 package com.android.car.radio;
 
+import android.annotation.ColorInt;
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.drawable.Drawable;
-import android.support.annotation.ColorInt;
 import android.util.AttributeSet;
 import android.widget.Button;
 
diff --git a/src/com/android/car/radio/RadioChannelColorMapper.java b/src/com/android/car/radio/RadioChannelColorMapper.java
index d83f127..146d62a 100644
--- a/src/com/android/car/radio/RadioChannelColorMapper.java
+++ b/src/com/android/car/radio/RadioChannelColorMapper.java
@@ -16,14 +16,15 @@
 
 package com.android.car.radio;
 
+import android.annotation.ColorInt;
+import android.annotation.NonNull;
 import android.content.Context;
-import android.hardware.radio.RadioManager;
-import android.support.annotation.ColorInt;
-import android.support.annotation.NonNull;
-import com.android.car.radio.service.RadioStation;
+import android.hardware.radio.ProgramSelector;
+
+import com.android.car.broadcastradio.support.platform.ProgramSelectorExt;
 
 /**
- * A class that will take a {@link RadioStation} and return its corresponding color. The colors
+ * A class that will take a {@link Program} and return its corresponding color. The colors
  * for different channels can be found on go/aae-ncar-por in the radio section.
  */
 public class RadioChannelColorMapper {
@@ -68,45 +69,37 @@
     }
 
     /**
-     * Convenience method for returning a color based on a {@link RadioStation}.
+     * Convenience method for returning a color based on a {@link Program}.
      *
-     * @see #getColorForStation(int, int)
+     * @see #getColorForStation
      */
     @ColorInt
-    public int getColorForStation(@NonNull RadioStation radioStation) {
-        return getColorForStation(radioStation.getRadioBand(), radioStation.getChannelNumber());
+    public int getColorForProgram(@NonNull ProgramSelector sel) {
+        if (!ProgramSelectorExt.isAmFmProgram(sel)
+                || !ProgramSelectorExt.hasId(sel, ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)) {
+            return mDefaultColor;
+        }
+        return getColorForChannel(sel.getFirstId(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY));
     }
 
     /**
      * Returns the color that should be used for the given radio band and channel. If a match cannot
      * be made, then {@link #mDefaultColor} is returned.
      *
-     * @param band One of {@link RadioManager}'s band values. (e.g. {@link RadioManager#BAND_AM}.
-     * @param channel The channel frequency in Hertz.
+     * @param freq The channel frequency in Hertz.
      */
     @ColorInt
-    public int getColorForStation(int band, int channel) {
-        switch (band) {
-            case RadioManager.BAND_AM:
-                if (channel < AM_LOW_THIRD_RANGE) {
-                    return mAmRange1Color;
-                } else if (channel > AM_HIGH_THIRD_RANGE) {
-                    return mAmRange3Color;
-                }
-
-                return mAmRange2Color;
-
-            case RadioManager.BAND_FM:
-                if (channel < FM_LOW_THIRD_RANGE) {
-                    return mFmRange1Color;
-                } else if (channel > FM_HIGH_THIRD_RANGE) {
-                    return mFmRange3Color;
-                }
-
-                return mFmRange2Color;
-
-            default:
-                return mDefaultColor;
+    public int getColorForChannel(long freq) {
+        if (ProgramSelectorExt.isAmFrequency(freq)) {
+                if (freq < AM_LOW_THIRD_RANGE) return mAmRange1Color;
+                else if (freq > AM_HIGH_THIRD_RANGE) return mAmRange3Color;
+                else return mAmRange2Color;
+        } else if (ProgramSelectorExt.isFmFrequency(freq)) {
+                if (freq < FM_LOW_THIRD_RANGE) return mFmRange1Color;
+                else if (freq > FM_HIGH_THIRD_RANGE) return mFmRange3Color;
+                else return mFmRange2Color;
+        } else {
+            return mDefaultColor;
         }
     }
 }
diff --git a/src/com/android/car/radio/RadioChannelFormatter.java b/src/com/android/car/radio/RadioChannelFormatter.java
deleted file mode 100644
index 76611d0..0000000
--- a/src/com/android/car/radio/RadioChannelFormatter.java
+++ /dev/null
@@ -1,98 +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.car.radio;
-
-import android.content.Context;
-import android.hardware.radio.RadioManager;
-import com.android.car.radio.service.RadioStation;
-
-import java.text.DecimalFormat;
-import java.util.Locale;
-
-/**
- * Common formatters for displaying channel numbers for various radio bands.
- */
-public final class RadioChannelFormatter {
-    private static final String FM_CHANNEL_FORMAT = "###.#";
-    private static final String AM_CHANNEL_FORMAT = "####";
-
-    private RadioChannelFormatter() {}
-
-    /**
-     * The formatter for AM radio stations.
-     */
-    public static final DecimalFormat FM_FORMATTER = new DecimalFormat(FM_CHANNEL_FORMAT);
-
-    /**
-     * The formatter for FM radio stations.
-     */
-    public static final DecimalFormat AM_FORMATTER = new DecimalFormat(AM_CHANNEL_FORMAT);
-
-    /**
-     * Convenience method to format a given {@link RadioStation} based on the value in
-     * {@link RadioStation#getRadioBand()}. If the band is invalid or support for its formatting is
-     * not available, then an empty String is returned.
-     *
-     * @param band One of the band values specified in {@link RadioManager}. For example,
-     *             {@link RadioManager#BAND_FM}.
-     * @param channelNumber The channel number to format. This value should be in KHz.
-     * @return A correctly formatted channel number or an empty string if one cannot be formed.
-     */
-    public static String formatRadioChannel(int band, int channelNumber) {
-        switch (band) {
-            case RadioManager.BAND_AM:
-                return AM_FORMATTER.format(channelNumber);
-
-            case RadioManager.BAND_FM:
-                // FM channels are displayed in KHz, so divide by 1000.
-                return FM_FORMATTER.format((float) channelNumber / 1000);
-
-            // TODO: Handle formats for AM and FM HD stations.
-
-            default:
-                return "";
-        }
-    }
-
-    /**
-     * Formats the given band value into a readable String.
-     *
-     * @param band One of the band values specified in {@link RadioManager}. For example,
-     *             {@link RadioManager#BAND_FM}.
-     * @return The formatted string or an empty string if the band is invalid.
-     */
-    public static String formatRadioBand(Context context, int band) {
-        String radioBandText;
-
-        switch (band) {
-            case RadioManager.BAND_AM:
-                radioBandText = context.getString(R.string.radio_am_text);
-                break;
-
-            case RadioManager.BAND_FM:
-                radioBandText = context.getString(R.string.radio_fm_text);
-                break;
-
-            // TODO: Handle formats for AM and FM HD stations.
-
-            default:
-                radioBandText = "";
-        }
-
-        return radioBandText.toUpperCase(Locale.getDefault());
-    }
-}
diff --git a/src/com/android/car/radio/RadioController.java b/src/com/android/car/radio/RadioController.java
index bf03fa2..713f64b 100644
--- a/src/com/android/car/radio/RadioController.java
+++ b/src/com/android/car/radio/RadioController.java
@@ -18,43 +18,43 @@
 
 import android.animation.ArgbEvaluator;
 import android.animation.ValueAnimator;
+import android.annotation.ColorInt;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.Activity;
-import android.app.LoaderManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.Loader;
 import android.content.ServiceConnection;
 import android.graphics.Color;
+import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
+import android.hardware.radio.RadioManager.ProgramInfo;
+import android.hardware.radio.RadioMetadata;
 import android.hardware.radio.RadioTuner;
 import android.media.AudioManager;
-import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
-import android.support.annotation.ColorInt;
-import android.support.annotation.Nullable;
-import android.text.TextUtils;
 import android.util.Log;
 import android.view.View;
 
+import com.android.car.broadcastradio.support.Program;
+import com.android.car.broadcastradio.support.platform.ProgramInfoExt;
+import com.android.car.broadcastradio.support.platform.ProgramSelectorExt;
 import com.android.car.radio.service.IRadioCallback;
 import com.android.car.radio.service.IRadioManager;
-import com.android.car.radio.service.RadioRds;
-import com.android.car.radio.service.RadioStation;
+import com.android.car.radio.storage.RadioStorage;
+import com.android.car.radio.utils.ProgramSelectorUtils;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * A controller that handles the display of metadata on the current radio station.
  */
-public class RadioController implements
-        RadioStorage.PresetsChangeListener,
-        RadioStorage.PreScannedChannelChangeListener,
-        LoaderManager.LoaderCallbacks<List<RadioStation>> {
+public class RadioController implements RadioStorage.PresetsChangeListener {
     private static final String TAG = "Em.RadioController";
-    private static final int CHANNEL_LOADER_ID = 0;
 
     /**
      * The percentage by which to darken the color that should be set on the status bar.
@@ -72,13 +72,16 @@
 
     private static final int CHANNEL_CHANGE_DURATION_MS = 200;
 
-    private int mCurrentChannelNumber = RadioStorage.INVALID_RADIO_CHANNEL;
+    private final ValueAnimator mAnimator = new ValueAnimator();
+    private int mCurrentlyDisplayedChannel;  // for animation purposes
+    private ProgramInfo mCurrentProgram;
 
     private final Activity mActivity;
     private IRadioManager mRadioManager;
 
     private View mRadioBackground;
     private boolean mShouldColorStatusBar;
+    private boolean mShouldColorBackground;
 
     /**
      * An additional layer on top of the background that should match the color of
@@ -93,11 +96,7 @@
     private final RadioChannelColorMapper mColorMapper;
     @ColorInt private int mCurrentBackgroundColor = INVALID_BACKGROUND_COLOR;
 
-    private PrescannedRadioStationAdapter mAdapter;
-    private PreScannedChannelLoader mChannelLoader;
-
     private final RadioDisplayController mRadioDisplayController;
-    private boolean mHasDualTuners;
 
     /**
      * Keeps track of if the user has manually muted the radio. This value is used to determine
@@ -108,28 +107,34 @@
 
     private final RadioStorage mRadioStorage;
 
-    /**
-     * The current radio band. This value is one of the BAND_* values from {@link RadioManager}.
-     * For example, {@link RadioManager#BAND_FM}.
-     */
-    private int mCurrentRadioBand = RadioStorage.INVALID_RADIO_BAND;
     private final String mAmBandString;
     private final String mFmBandString;
 
-    private RadioRds mCurrentRds;
-
-    private RadioStationChangeListener mStationChangeListener;
+    private List<ProgramInfoChangeListener> mProgramInfoChangeListeners = new ArrayList<>();
+    private List<RadioServiceConnectionListener> mRadioServiceConnectionListeners =
+            new ArrayList<>();
 
     /**
      * Interface for a class that will be notified when the current radio station has been changed.
      */
-    public interface RadioStationChangeListener {
+    public interface ProgramInfoChangeListener {
         /**
          * Called when the current radio station has changed in the radio.
          *
-         * @param station The current radio station.
+         * @param info The current radio station.
          */
-        void onRadioStationChanged(RadioStation station);
+        void onProgramInfoChanged(@NonNull ProgramInfo info);
+    }
+
+    /**
+     * Interface for a class that will be notified when RadioService is successfuly bound
+     */
+    public interface RadioServiceConnectionListener {
+
+        /**
+         * Called when the RadioService is successfully connected
+         */
+        void onRadioServiceConnected();
     }
 
     public RadioController(Activity activity) {
@@ -143,6 +148,7 @@
 
         mRadioStorage = RadioStorage.getInstance(mActivity);
         mRadioStorage.addPresetsChangeListener(this);
+        mShouldColorBackground = true;
     }
 
     /**
@@ -184,10 +190,39 @@
     }
 
     /**
-     * Sets the listener that will be notified whenever the radio station changes.
+     * Set whether this controller should update the background color.
+     * This behavior is enabled by defaullt
      */
-    public void setRadioStationChangeListener(RadioStationChangeListener listener) {
-        mStationChangeListener = listener;
+    public void setShouldColorBackground(boolean shouldColorBackground) {
+        mShouldColorBackground = shouldColorBackground;
+    }
+
+    /**
+     * Adds a listener that will be notified whenever the radio station changes.
+     */
+    public void addProgramInfoChangeListener(ProgramInfoChangeListener listener) {
+        mProgramInfoChangeListeners.add(listener);
+    }
+
+    /**
+     * Removes a listener that will be notified whenever the radio station changes.
+     */
+    public void removeProgramInfoChangeListener(ProgramInfoChangeListener listener) {
+        mProgramInfoChangeListeners.remove(listener);
+    }
+
+    /**
+     * Sets the listeners that will be notified when the radio service is connected.
+     */
+    public void addRadioServiceConnectionListener(RadioServiceConnectionListener listener) {
+        mRadioServiceConnectionListeners.add(listener);
+    }
+
+    /**
+     * Removes a listener that will be notified when the radio service is connected.
+     */
+    public void removeRadioServiceConnectionListener(RadioServiceConnectionListener listener) {
+        mRadioServiceConnectionListeners.remove(listener);
     }
 
     /**
@@ -199,7 +234,8 @@
             Log.d(TAG, "starting radio");
         }
 
-        Intent bindIntent = new Intent(mActivity, RadioService.class);
+        Intent bindIntent = new Intent(RadioService.ACTION_UI_SERVICE, null /* uri */,
+                mActivity, RadioService.class);
         if (!mActivity.bindService(bindIntent, mServiceConnection, Context.BIND_AUTO_CREATE)) {
             Log.e(TAG, "Failed to connect to RadioService.");
         }
@@ -217,35 +253,14 @@
         }
 
         try {
-            RadioStation station = mRadioManager.getCurrentRadioStation();
-
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "updateRadioDisplay(); current station: " + station);
-            }
-
-            mHasDualTuners = mRadioManager.hasDualTuners();
-
-            if (mHasDualTuners) {
-                initializeDualTunerController();
-            } else {
-                mRadioDisplayController.setSingleChannelDisplay(mRadioBackground);
-            }
-
-            // Update the AM/FM band display.
-            mCurrentRadioBand = station.getRadioBand();
-            updateAmFmDisplayState();
-
-            // Update the channel number.
-            setRadioChannel(station.getChannelNumber());
+            mRadioDisplayController.setSingleChannelDisplay(mRadioBackground);
 
             // Ensure the play button properly reflects the current mute state.
             mRadioDisplayController.setPlayPauseButtonState(mRadioManager.isMuted());
 
-            mCallback.onRadioMetadataChanged(station.getRds());
-
-            if (mStationChangeListener != null) {
-                mStationChangeListener.onRadioStationChanged(station);
-            }
+            // TODO(b/73950974): use callback only
+            ProgramInfo current = mRadioManager.getCurrentProgramInfo();
+            if (current != null) mCallback.onCurrentProgramInfoChanged(current);
         } catch (RemoteException e) {
             Log.e(TAG, "updateRadioDisplay(); remote exception: " + e.getMessage());
         }
@@ -254,123 +269,55 @@
     /**
      * Tunes the radio to the given channel if it is valid and a {@link RadioTuner} has been opened.
      */
-    public void tuneToRadioChannel(RadioStation radioStation) {
-        if (mRadioManager == null) {
-            return;
-        }
+    public void tune(ProgramSelector sel) {
+        if (mRadioManager == null) return;
 
         try {
-            mRadioManager.tune(radioStation);
-        } catch (RemoteException e) {
-            Log.e(TAG, "tuneToRadioChannel(); remote exception: " + e.getMessage());
+            mRadioManager.tune(sel);
+        } catch (RemoteException ex) {
+            Log.e(TAG, "Failed to tune", ex);
         }
     }
 
     /**
      * Returns the band this radio is currently tuned to.
+     *
+     * TODO(b/73950974): don't be AM/FM exclusive
      */
     public int getCurrentRadioBand() {
-        return mCurrentRadioBand;
+        return ProgramSelectorUtils.getRadioBand(mCurrentProgram.getSelector());
     }
 
     /**
      * Returns the radio station that is currently playing on the radio. If this controller is
      * not connected to the {@link RadioService} or a radio station cannot be retrieved, then
      * {@code null} is returned.
+     *
+     * TODO(b/73950974): use callback only
      */
     @Nullable
-    public RadioStation getCurrentRadioStation() {
-        if (mRadioManager == null) {
-            return null;
-        }
-
-        try {
-            return mRadioManager.getCurrentRadioStation();
-        } catch (RemoteException e) {
-            Log.e(TAG, "getCurrentRadioStation(); error retrieving current station: "
-                    + e.getMessage());
-        }
-
-        return null;
+    public ProgramInfo getCurrentProgramInfo() {
+        return mCurrentProgram;
     }
 
     /**
-     * Opens the given current radio band. Currently, this only supports FM and AM bands.
+     * Switch radio band. Currently, this only supports FM and AM bands.
      *
-     * @param radioBand One of {@link RadioManager#BAND_FM}, {@link RadioManager#BAND_AM},
-     *                  {@link RadioManager#BAND_FM_HD} or {@link RadioManager#BAND_AM_HD}.
+     * @param radioBand One of {@link RadioManager#BAND_FM}, {@link RadioManager#BAND_AM}.
      */
-    public void openRadioBand(int radioBand) {
-        if (mRadioManager == null || radioBand == mCurrentRadioBand) {
-            return;
-        }
-
-        // Reset the channel number so that we do not animate number changes between band changes.
-        mCurrentChannelNumber = RadioStorage.INVALID_RADIO_CHANNEL;
-
-        setCurrentRadioBand(radioBand);
-        mRadioStorage.storeRadioBand(mCurrentRadioBand);
-
+    public void switchBand(int radioBand) {
         try {
-            mRadioManager.openRadioBand(radioBand);
-
-            updateAmFmDisplayState();
-
-            // Sets the initial mute state. This will resolve the mute state should be if an
-            // {@link AudioManager#AUDIOFOCUS_LOSS_TRANSIENT} event is received followed by an
-            // {@link AudioManager#AUDIOFOCUS_GAIN} event. In this case, the radio will un-mute itself
-            // if the user has not muted beforehand.
-            if (mUserHasMuted) {
-                mRadioManager.mute();
-            }
-
-            // Ensure the play button properly reflects the current mute state.
-            mRadioDisplayController.setPlayPauseButtonState(mRadioManager.isMuted());
-
-            maybeTuneToStoredRadioChannel();
+            mRadioManager.switchBand(radioBand);
         } catch (RemoteException e) {
-            Log.e(TAG, "openRadioBand(); remote exception: " + e.getMessage());
+            Log.e(TAG, "Couldn't switch band", e);
         }
     }
 
     /**
-     * Attempts to tune to the last played radio channel for a particular band. For example, if
-     * the user switches to the AM band from FM, this method will attempt to tune to the last
-     * AM band that the user was on.
-     *
-     * <p>If a stored radio station cannot be found, then this method will initiate a seek so that
-     * the radio is always on a valid radio station.
+     * Delegates to the {@link RadioDisplayController} to highlight the radio band.
      */
-    private void maybeTuneToStoredRadioChannel() {
-        mCurrentChannelNumber = mRadioStorage.getStoredRadioChannel(mCurrentRadioBand);
-
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, String.format("maybeTuneToStoredRadioChannel(); band: %s, channel %s",
-                    mCurrentRadioBand, mCurrentChannelNumber));
-        }
-
-        // Tune to a stored radio channel if it exists.
-        if (mCurrentChannelNumber != RadioStorage.INVALID_RADIO_CHANNEL) {
-            RadioStation station = new RadioStation(mCurrentChannelNumber, 0 /* subchannel */,
-                    mCurrentRadioBand, mCurrentRds);
-            tuneToRadioChannel(station);
-        } else {
-            // Otherwise, ensure that the radio is on a valid radio station (i.e. it will not
-            // start playing static) by initiating a seek.
-            try {
-                mRadioManager.seekForward();
-            } catch (RemoteException e) {
-                Log.e(TAG, "maybeTuneToStoredRadioChannel(); remote exception: " + e.getMessage());
-            }
-        }
-    }
-
-    /**
-     * Delegates to the {@link RadioDisplayController} to highlight the radio band that matches
-     * up to {@link #mCurrentRadioBand}.
-     */
-    private void updateAmFmDisplayState() {
-        switch (mCurrentRadioBand) {
+    private void updateAmFmDisplayState(int band) {
+        switch (band) {
             case RadioManager.BAND_FM:
                 mRadioDisplayController.setChannelBand(mFmBandString);
                 break;
@@ -386,88 +333,58 @@
         }
     }
 
-    /**
-     * Sets the radio channel to display.
-     * @param channel The radio channel frequency in Hz.
-     */
-    private void setRadioChannel(int channel) {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "Setting radio channel: " + channel);
-        }
+    // TODO(b/73950974): move channel animation to RadioDisplayController
+    private void updateRadioChannelDisplay(@NonNull ProgramSelector sel) {
+        int priType = sel.getPrimaryId().getType();
 
-        if (channel <= 0) {
-            mCurrentChannelNumber = channel;
+        mAnimator.cancel();
+
+        if (!ProgramSelectorExt.isAmFmProgram(sel)
+                || !ProgramSelectorExt.hasId(sel, ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)) {
+            // channel animation is implemented for AM/FM only
+            mCurrentlyDisplayedChannel = 0;
             mRadioDisplayController.setChannelNumber("");
+
+            updateAmFmDisplayState(RadioStorage.INVALID_RADIO_BAND);
             return;
         }
 
-        if (mHasDualTuners) {
-            int position = mAdapter.getIndexOrInsertForStation(channel, mCurrentRadioBand);
-            mRadioDisplayController.setCurrentStationInList(position);
-        }
+        int freq = (int)sel.getFirstId(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY);
 
-        switch (mCurrentRadioBand) {
-            case RadioManager.BAND_FM:
-                setRadioChannelForFm(channel);
-                break;
+        boolean wasAm = ProgramSelectorExt.isAmFrequency(mCurrentlyDisplayedChannel);
+        boolean wasFm = ProgramSelectorExt.isFmFrequency(mCurrentlyDisplayedChannel);
+        boolean isAm = ProgramSelectorExt.isAmFrequency(freq);
+        int band = isAm ? RadioManager.BAND_AM : RadioManager.BAND_FM;
 
-            case RadioManager.BAND_AM:
-                setRadioChannelForAm(channel);
-                break;
+        updateAmFmDisplayState(band);
 
-            // TODO: Support BAND_FM_HD and BAND_AM_HD.
-
-            default:
-                // Do nothing and don't check presets, so return here.
-                return;
-        }
-
-        mCurrentChannelNumber = channel;
-
-        mRadioDisplayController.setChannelIsPreset(
-                mRadioStorage.isPreset(channel, mCurrentRadioBand));
-
-        mRadioStorage.storeRadioChannel(mCurrentRadioBand, mCurrentChannelNumber);
-
-        maybeUpdateBackgroundColor();
-    }
-
-    private void setRadioChannelForAm(int channel) {
-        // No need for animation if radio channel has never been set.
-        if (mCurrentChannelNumber == RadioStorage.INVALID_RADIO_CHANNEL) {
+        if (isAm && wasAm || !isAm && wasFm) {
+            mAnimator.setIntValues((int)mCurrentlyDisplayedChannel, (int)freq);
+            mAnimator.setDuration(CHANNEL_CHANGE_DURATION_MS);
+            mAnimator.addUpdateListener(animation -> mRadioDisplayController.setChannelNumber(
+                    ProgramSelectorExt.formatAmFmFrequency((int)animation.getAnimatedValue(),
+                            ProgramSelectorExt.NAME_NO_MODULATION)));
+            mAnimator.start();
+        } else {
+            // it's a different band - don't animate
             mRadioDisplayController.setChannelNumber(
-                    RadioChannelFormatter.AM_FORMATTER.format(channel));
-            return;
+                    ProgramSelectorExt.getDisplayName(sel, ProgramSelectorExt.NAME_NO_MODULATION));
         }
+        mCurrentlyDisplayedChannel = freq;
 
-        animateRadioChannelChange(mCurrentChannelNumber, channel, mAmAnimatorListener);
-    }
-
-    private void setRadioChannelForFm(int channel) {
-        // FM channels are displayed in Khz. e.g. 88500 is displayed as 88.5.
-        float channelInKHz = (float) channel / 1000;
-
-        // No need for animation if radio channel has never been set.
-        if (mCurrentChannelNumber == RadioStorage.INVALID_RADIO_CHANNEL) {
-            mRadioDisplayController.setChannelNumber(
-                    RadioChannelFormatter.FM_FORMATTER.format(channelInKHz));
-            return;
-        }
-
-        float startChannelNumber = (float) mCurrentChannelNumber / 1000;
-        animateRadioChannelChange(startChannelNumber, channelInKHz, mFmAnimatorListener);
+        maybeUpdateBackgroundColor(freq);
     }
 
     /**
      * Checks if the color of the radio background should be changed, and if so, animates that
      * color change.
      */
-    private void maybeUpdateBackgroundColor() {
-        if (mRadioBackground == null) {
+    private void maybeUpdateBackgroundColor(int channel) {
+        if (mRadioBackground == null || !mShouldColorBackground) {
             return;
         }
 
-        int newColor = mColorMapper.getColorForStation(mCurrentRadioBand, mCurrentChannelNumber);
+        int newColor = mColorMapper.getColorForChannel(channel);
 
         // No animation required if the colors are the same.
         if (newColor == mCurrentBackgroundColor) {
@@ -517,45 +434,11 @@
     }
 
     /**
-     * Animates the text in channel number from the given starting value to the given
-     * end value.
-     */
-    private void animateRadioChannelChange(float startValue, float endValue,
-            ValueAnimator.AnimatorUpdateListener listener) {
-        ValueAnimator animator = new ValueAnimator();
-        animator.setObjectValues(startValue, endValue);
-        animator.setDuration(CHANNEL_CHANGE_DURATION_MS);
-        animator.addUpdateListener(listener);
-        animator.start();
-    }
-
-    /**
      * Clears all metadata including song title, artist and station information.
      */
     private void clearMetadataDisplay() {
-        mCurrentRds = null;
-
-        mRadioDisplayController.setCurrentSongArtistOrStation(null);
-        mRadioDisplayController.setCurrentSongTitle(null);
-    }
-
-    /**
-     * Sets the internal {@link #mCurrentRadioBand} to be the given radio band. Will also take care
-     * of restarting a load of the pre-scanned radio stations for the given band if there are dual
-     * tuners on the device.
-     */
-    private void setCurrentRadioBand(int radioBand) {
-        if (mCurrentRadioBand == radioBand) {
-            return;
-        }
-
-        mCurrentRadioBand = radioBand;
-
-        if (mChannelLoader != null) {
-            mAdapter.setStations(new ArrayList<>());
-            mChannelLoader.setCurrentRadioBand(radioBand);
-            mChannelLoader.forceLoad();
-        }
+        mRadioDisplayController.setCurrentStation(null);
+        mRadioDisplayController.setCurrentSongTitleAndArtist(null, null);
     }
 
     /**
@@ -580,7 +463,6 @@
 
         mActivity.unbindService(mServiceConnection);
         mRadioStorage.removePresetsChangeListener(this);
-        mRadioStorage.removePreScannedChannelChangeListener(this);
 
         if (mRadioManager != null) {
             try {
@@ -593,187 +475,53 @@
         close();
     }
 
-    /**
-     * Initializes all the extra components that are needed if this radio has dual tuners.
-     */
-    private void initializeDualTunerController() {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "initializeDualTunerController()");
-        }
-
-        mRadioStorage.addPreScannedChannelChangeListener(RadioController.this);
-
-        if (mAdapter == null) {
-            mAdapter = new PrescannedRadioStationAdapter();
-        }
-
-        mRadioDisplayController.setChannelListDisplay(mRadioBackground, mAdapter);
-
-        // Initialize the loader that will load the pre-scanned channels for the current band.
-        mActivity.getLoaderManager().initLoader(CHANNEL_LOADER_ID, null /* args */,
-                RadioController.this /* callback */).forceLoad();
-    }
-
     @Override
     public void onPresetsRefreshed() {
         // Check if the current channel's preset status has changed.
-        mRadioDisplayController.setChannelIsPreset(
-                mRadioStorage.isPreset(mCurrentChannelNumber, mCurrentRadioBand));
+        ProgramInfo info = mCurrentProgram;
+        boolean isPreset = (info != null) && mRadioStorage.isPreset(info.getSelector());
+        mRadioDisplayController.setChannelIsPreset(isPreset);
     }
 
-    @Override
-    public void onPreScannedChannelChange(int radioBand) {
-        // If pre-scanned channels have changed for the current radio band, then refresh the list
-        // that is currently being displayed.
-        if (radioBand == mCurrentRadioBand && mChannelLoader != null) {
-            mChannelLoader.forceLoad();
-        }
-    }
-
-    @Override
-    public Loader<List<RadioStation>> onCreateLoader(int id, Bundle args) {
-        // Only one loader, so no need to check for id.
-        mChannelLoader = new PreScannedChannelLoader(mActivity /* context */);
-        mChannelLoader.setCurrentRadioBand(mCurrentRadioBand);
-
-        return mChannelLoader;
-    }
-
-    @Override
-    public void onLoadFinished(Loader<List<RadioStation>> loader,
-            List<RadioStation> preScannedStations) {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            int size = preScannedStations == null ? 0 : preScannedStations.size();
-            Log.d(TAG, "onLoadFinished(); number of pre-scanned stations: " + size);
-        }
-
-        if (Log.isLoggable(TAG, Log.VERBOSE) && preScannedStations != null) {
-            for (RadioStation station : preScannedStations) {
-                Log.v(TAG, "station: " + station.toString());
+    /**
+     * Gets a list of programs from the radio tuner's background scan
+     */
+    public List<ProgramInfo> getProgramList() {
+        if (mRadioManager != null) {
+            try {
+                return mRadioManager.getProgramList();
+            } catch (RemoteException e) {
+                Log.e(TAG, "getProgramList(); remote exception: " + e.getMessage());
             }
         }
-
-        mAdapter.setStations(preScannedStations);
-
-        int position = mAdapter.setStartingStation(mCurrentChannelNumber, mCurrentRadioBand);
-        mRadioDisplayController.setCurrentStationInList(position);
+        return null;
     }
 
-    @Override
-    public void onLoaderReset(Loader<List<RadioStation>> loader) {}
-
-    /**
-     * Value animator for AM values.
-     */
-    private ValueAnimator.AnimatorUpdateListener mAmAnimatorListener =
-            new ValueAnimator.AnimatorUpdateListener() {
-                public void onAnimationUpdate(ValueAnimator animation) {
-                    mRadioDisplayController.setChannelNumber(
-                            RadioChannelFormatter.AM_FORMATTER.format(
-                                    animation.getAnimatedValue()));
-                }
-            };
-
-    /**
-     * Value animator for FM values.
-     */
-    private ValueAnimator.AnimatorUpdateListener mFmAnimatorListener =
-            new ValueAnimator.AnimatorUpdateListener() {
-                public void onAnimationUpdate(ValueAnimator animation) {
-                    mRadioDisplayController.setChannelNumber(
-                            RadioChannelFormatter.FM_FORMATTER.format(
-                                    animation.getAnimatedValue()));
-                }
-            };
-
     private final IRadioCallback.Stub mCallback = new IRadioCallback.Stub() {
         @Override
-        public void onRadioStationChanged(RadioStation station) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "onRadioStationChanged: " + station);
-            }
+        public void onCurrentProgramInfoChanged(ProgramInfo info) {
+            mCurrentProgram = Objects.requireNonNull(info);
+            ProgramSelector sel = info.getSelector();
 
-            if (station == null) {
-                return;
-            }
+            updateRadioChannelDisplay(sel);
 
-            if (mCurrentChannelNumber != station.getChannelNumber()) {
-                setRadioChannel(station.getChannelNumber());
-            }
+            mRadioDisplayController.setCurrentStation(
+                    ProgramInfoExt.getProgramName(info, ProgramInfoExt.NAME_NO_CHANNEL_FALLBACK));
+            RadioMetadata meta = ProgramInfoExt.getMetadata(mCurrentProgram);
+            mRadioDisplayController.setCurrentSongTitleAndArtist(
+                    meta.getString(RadioMetadata.METADATA_KEY_TITLE),
+                    meta.getString(RadioMetadata.METADATA_KEY_ARTIST));
 
-            onRadioMetadataChanged(station.getRds());
+            mRadioDisplayController.setChannelIsPreset(mRadioStorage.isPreset(sel));
 
             // Notify that the current radio station has changed.
-            if (mStationChangeListener != null) {
-                try {
-                    mStationChangeListener.onRadioStationChanged(
-                            mRadioManager.getCurrentRadioStation());
-                } catch (RemoteException e) {
-                    Log.e(TAG, "tuneToRadioChannel(); remote exception: " + e.getMessage());
+            if (mProgramInfoChangeListeners != null) {
+                for (ProgramInfoChangeListener listener : mProgramInfoChangeListeners) {
+                    listener.onProgramInfoChanged(info);
                 }
             }
         }
 
-        /**
-         * Updates radio information based on the given {@link RadioRds}.
-         */
-        @Override
-        public void onRadioMetadataChanged(RadioRds radioRds) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "onMetadataChanged(); metadata: " + radioRds);
-            }
-
-            clearMetadataDisplay();
-
-            if (radioRds == null) {
-                return;
-            }
-
-            mCurrentRds = radioRds;
-
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "mCurrentRds: " + mCurrentRds);
-            }
-
-            String programService = radioRds.getProgramService();
-            String artistMetadata = radioRds.getSongArtist();
-
-            mRadioDisplayController.setCurrentSongArtistOrStation(
-                    TextUtils.isEmpty(artistMetadata) ? programService : artistMetadata);
-            mRadioDisplayController.setCurrentSongTitle(radioRds.getSongTitle());
-
-            // Since new metadata exists, update the preset that is stored in the database if
-            // it exists.
-            if (TextUtils.isEmpty(programService)) {
-                return;
-            }
-
-            RadioStation station = new RadioStation(mCurrentChannelNumber, 0 /* subchannel */,
-                    mCurrentRadioBand, radioRds);
-            boolean isPreset = mRadioStorage.isPreset(station);
-
-            if (isPreset) {
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "Current channel is a preset; updating metadata in the database.");
-                }
-
-                mRadioStorage.storePreset(station);
-            }
-        }
-
-        @Override
-        public void onRadioBandChanged(int radioBand) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "onRadioBandChanged: " + radioBand);
-            }
-
-            setCurrentRadioBand(radioBand);
-            updateAmFmDisplayState();
-
-            // Check that the radio channel is being correctly formatted.
-            setRadioChannel(mCurrentChannelNumber);
-        }
-
         @Override
         public void onRadioMuteChanged(boolean isMuted) {
             mRadioDisplayController.setPlayPauseButtonState(isMuted);
@@ -789,37 +537,16 @@
     private final View.OnClickListener mBackwardSeekClickListener = new View.OnClickListener() {
         @Override
         public void onClick(View v) {
-            if (mRadioManager == null) {
-                return;
-            }
+            if (mRadioManager == null) return;
 
+            // TODO(b/73950974): show some kind of animation
             clearMetadataDisplay();
 
-            if (!mHasDualTuners) {
-                try {
-                    mRadioManager.seekBackward();
-                } catch (RemoteException e) {
-                    Log.e(TAG, "backwardSeek(); remote exception: " + e.getMessage());
-                }
-                return;
-            }
-
-            RadioStation prevStation = mAdapter.getPrevStation();
-
-            if (prevStation != null) {
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "Seek backwards to station: " + prevStation);
-                }
-
-                // Tune to the previous station, and then update the UI to reflect that tune.
-                try {
-                    mRadioManager.tune(prevStation);
-                } catch (RemoteException e) {
-                    Log.e(TAG, "backwardSeek(); remote exception: " + e.getMessage());
-                }
-
-                int position = mAdapter.getCurrentPosition();
-                mRadioDisplayController.setCurrentStationInList(position);
+            try {
+                // TODO(b/73950974): watch for timeout and if it happens, display metadata back
+                mRadioManager.seekBackward();
+            } catch (RemoteException e) {
+                Log.e(TAG, "backwardSeek(); remote exception: " + e.getMessage());
             }
         }
     };
@@ -827,37 +554,14 @@
     private final View.OnClickListener mForwardSeekClickListener = new View.OnClickListener() {
         @Override
         public void onClick(View v) {
-            if (mRadioManager == null) {
-                return;
-            }
+            if (mRadioManager == null) return;
 
             clearMetadataDisplay();
 
-            if (!mHasDualTuners) {
-                try {
-                    mRadioManager.seekForward();
-                } catch (RemoteException e) {
-                    Log.e(TAG, "forwardSeek(); remote exception: " + e.getMessage());
-                }
-                return;
-            }
-
-            RadioStation nextStation = mAdapter.getNextStation();
-
-            if (nextStation != null) {
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "Seek forward to station: " + nextStation);
-                }
-
-                // Tune to the next station, and then update the UI to reflect that tune.
-                try {
-                    mRadioManager.tune(nextStation);
-                } catch (RemoteException e) {
-                    Log.e(TAG, "forwardSeek(); remote exception: " + e.getMessage());
-                }
-
-                int position = mAdapter.getCurrentPosition();
-                mRadioDisplayController.setCurrentStationInList(position);
+            try {
+                mRadioManager.seekForward();
+            } catch (RemoteException e) {
+                Log.e(TAG, "Couldn't seek forward", e);
             }
         }
     };
@@ -899,27 +603,16 @@
         // there aren't multiple writes if the user presses the button quickly.
         @Override
         public void onClick(View v) {
-            if (mCurrentChannelNumber == RadioStorage.INVALID_RADIO_CHANNEL) {
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "Attempting to store invalid radio station as a preset. Ignoring");
-                }
+            ProgramInfo info = mCurrentProgram;
+            if (info == null) return;
 
-                return;
-            }
-
-            RadioStation station = new RadioStation(mCurrentChannelNumber, 0 /* subchannel */,
-                    mCurrentRadioBand, mCurrentRds);
-            boolean isPreset = mRadioStorage.isPreset(station);
-
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "Toggling preset for " + station
-                        + "\n\tIs currently a preset: " + isPreset);
-            }
+            ProgramSelector sel = mCurrentProgram.getSelector();
+            boolean isPreset = mRadioStorage.isPreset(sel);
 
             if (isPreset) {
-                mRadioStorage.removePreset(station);
+                mRadioStorage.removePreset(sel);
             } else {
-                mRadioStorage.storePreset(station);
+                mRadioStorage.storePreset(Program.fromProgramInfo(info));
             }
 
             // Update the UI immediately. If the preset failed for some reason, the RadioStorage
@@ -950,25 +643,13 @@
                     mRadioErrorDisplay.setVisibility(View.GONE);
                 }
 
-                mHasDualTuners = mRadioManager.hasDualTuners();
-
-                if (mHasDualTuners) {
-                    initializeDualTunerController();
-                } else {
-                    mRadioDisplayController.setSingleChannelDisplay(mRadioBackground);
-                }
+                mRadioDisplayController.setSingleChannelDisplay(mRadioBackground);
 
                 mRadioManager.addRadioTunerCallback(mCallback);
 
-                int radioBand = mRadioStorage.getStoredRadioBand();
-
-                // Upon successful connection, open the radio.
-                openRadioBand(radioBand);
-                maybeTuneToStoredRadioChannel();
-
-                if (mStationChangeListener != null) {
-                    mStationChangeListener.onRadioStationChanged(
-                            mRadioManager.getCurrentRadioStation());
+                // Notify listeners
+                for (RadioServiceConnectionListener listener : mRadioServiceConnectionListeners) {
+                    listener.onRadioServiceConnected();
                 }
             } catch (RemoteException e) {
                 Log.e(TAG, "onServiceConnected(); remote exception: " + e.getMessage());
diff --git a/src/com/android/car/radio/RadioDatabase.java b/src/com/android/car/radio/RadioDatabase.java
deleted file mode 100644
index a6dae76..0000000
--- a/src/com/android/car/radio/RadioDatabase.java
+++ /dev/null
@@ -1,402 +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.car.radio;
-
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.os.Looper;
-import android.support.annotation.NonNull;
-import android.support.annotation.WorkerThread;
-import android.text.TextUtils;
-import android.util.Log;
-import com.android.car.radio.service.RadioRds;
-import com.android.car.radio.service.RadioStation;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A helper class that manages all operations relating to the database. This class should not
- * be accessed directly. Instead, {@link RadioStorage} interfaces directly with it.
- */
-public final class RadioDatabase extends SQLiteOpenHelper {
-    private static final String TAG = "Em.RadioDatabase";
-
-    private static final String DATABASE_NAME = "RadioDatabase";
-    private static final int DATABASE_VERSION = 1;
-
-    /**
-     * The table that holds all the user's currently stored presets.
-     */
-    private static final class RadioPresetsTable {
-        public static final String NAME = "presets_table";
-
-        private static final class Columns {
-            public static final String CHANNEL_NUMBER = "channel_number";
-            public static final String SUB_CHANNEL = "sub_channel";
-            public static final String BAND = "band";
-            public static final String PROGRAM_SERVICE = "program_service";
-        }
-    }
-
-    /**
-     * Creates the radio presets table. A channel number together with its subchannel number
-     * represents the primary key
-     */
-    private static final String CREATE_PRESETS_TABLE =
-            "CREATE TABLE " + RadioPresetsTable.NAME + " ("
-                    + RadioPresetsTable.Columns.CHANNEL_NUMBER + " INTEGER NOT NULL, "
-                    + RadioPresetsTable.Columns.SUB_CHANNEL + " INTEGER NOT NULL, "
-                    + RadioPresetsTable.Columns.BAND + " INTEGER NOT NULL, "
-                    + RadioPresetsTable.Columns.PROGRAM_SERVICE + " TEXT, "
-                    + "PRIMARY KEY ("
-                    + RadioPresetsTable.Columns.CHANNEL_NUMBER + ", "
-                    + RadioPresetsTable.Columns.SUB_CHANNEL + "));";
-
-    private static final String DELETE_PRESETS_TABLE =
-            "DROP TABLE IF EXISTS " + RadioPresetsTable.NAME;
-
-    /**
-     * Query to return the entire {@link RadioPresetsTable}.
-     */
-    private static final String GET_ALL_PRESETS =
-            "SELECT * FROM " + RadioPresetsTable.NAME
-                    + " ORDER BY " + RadioPresetsTable.Columns.CHANNEL_NUMBER
-                    + ", " + RadioPresetsTable.Columns.SUB_CHANNEL;
-
-    /**
-     * The WHERE clause for a delete preset operation. A preset is identified uniquely by its
-     * channel and subchannel number.
-     */
-    private static final String DELETE_PRESETS_WHERE_CLAUSE =
-            RadioPresetsTable.Columns.CHANNEL_NUMBER + " = ? AND "
-            + RadioPresetsTable.Columns.SUB_CHANNEL + " = ?";
-
-    /**
-     * The table that holds all radio stations have have been pre-scanned by a secondary tuner.
-     */
-    private static final class PreScannedStationsTable {
-        public static final String NAME = "pre_scanned_table";
-
-        private static final class Columns {
-            public static final String CHANNEL_NUMBER = "channel_number";
-            public static final String SUB_CHANNEL = "sub_channel";
-            public static final String BAND = "band";
-            public static final String PROGRAM_SERVICE = "program_service";
-        }
-    }
-
-    /**
-     * Creates the radio pre-scanned table. A channel number together with its subchannel number
-     * represents the primary key
-     */
-    private static final String CREATE_PRE_SCAN_TABLE =
-            "CREATE TABLE " + PreScannedStationsTable.NAME + " ("
-                    + PreScannedStationsTable.Columns.CHANNEL_NUMBER + " INTEGER NOT NULL, "
-                    + PreScannedStationsTable.Columns.SUB_CHANNEL + " INTEGER NOT NULL, "
-                    + PreScannedStationsTable.Columns.BAND + " INTEGER NOT NULL, "
-                    + PreScannedStationsTable.Columns.PROGRAM_SERVICE + " TEXT, "
-                    + "PRIMARY KEY ("
-                    + PreScannedStationsTable.Columns.CHANNEL_NUMBER + ", "
-                    + PreScannedStationsTable.Columns.SUB_CHANNEL + "));";
-
-    private static final String DELETE_PRE_SCAN_TABLE =
-            "DROP TABLE IF EXISTS " + PreScannedStationsTable.NAME;
-
-    /**
-     * Query to return all the pre-scanned stations for a particular radio band.
-     */
-    private static final String GET_ALL_PRE_SCAN_FOR_BAND =
-            "SELECT * FROM " + PreScannedStationsTable.NAME
-                    + " WHERE " + PreScannedStationsTable.Columns.BAND  + " = ? "
-                    + " ORDER BY " + PreScannedStationsTable.Columns.CHANNEL_NUMBER
-                    + ", " + PreScannedStationsTable.Columns.SUB_CHANNEL;
-
-    /**
-     * The WHERE clause for a delete operation that will remove all pre-scanned stations for a
-     * paritcular radio band.
-     */
-    private static final String DELETE_PRE_SCAN_WHERE_CLAUSE =
-            PreScannedStationsTable.Columns.BAND + " = ?";
-
-    public RadioDatabase(Context context) {
-        super(context, DATABASE_NAME, null /* factory */, DATABASE_VERSION);
-    }
-
-    /**
-     * Returns a list of all user defined radio presets sorted by channel number and then sub
-     * channel number. If there are no presets, then an empty {@link List} is returned.
-     */
-    @WorkerThread
-    public List<RadioStation> getAllPresets() {
-        assertNotMainThread();
-
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "getAllPresets()");
-        }
-
-        SQLiteDatabase db = getReadableDatabase();
-        List<RadioStation> presets = new ArrayList<>();
-        Cursor cursor = null;
-
-        db.beginTransaction();
-        try {
-            cursor = db.rawQuery(GET_ALL_PRESETS, null /* selectionArgs */);
-            while (cursor.moveToNext()) {
-                int channel = cursor.getInt(
-                        cursor.getColumnIndexOrThrow(RadioPresetsTable.Columns.CHANNEL_NUMBER));
-                int subChannel = cursor.getInt(
-                        cursor.getColumnIndexOrThrow(RadioPresetsTable.Columns.SUB_CHANNEL));
-                int band = cursor.getInt(
-                        cursor.getColumnIndexOrThrow(RadioPresetsTable.Columns.BAND));
-                String programService = cursor.getString(
-                        cursor.getColumnIndexOrThrow(RadioPresetsTable.Columns.PROGRAM_SERVICE));
-
-                RadioRds rds = null;
-                if (!TextUtils.isEmpty(programService)) {
-                    rds = new RadioRds(programService, null /* songArtist */, null /* songTitle */);
-                }
-
-                presets.add(new RadioStation(channel, subChannel, band, rds));
-            }
-
-            db.setTransactionSuccessful();
-        } finally {
-            if (cursor != null) {
-                cursor.close();
-            }
-
-            db.endTransaction();
-            db.close();
-        }
-
-        return presets;
-    }
-
-    /**
-     * Inserts that given {@link RadioStation} as a preset into the database. The given station
-     * will replace any existing station in the database if there is a conflict.
-     *
-     * @return {@code true} if the operation succeeded.
-     */
-    @WorkerThread
-    public boolean insertPreset(RadioStation preset) {
-        assertNotMainThread();
-
-        ContentValues values = new ContentValues();
-        values.put(RadioPresetsTable.Columns.CHANNEL_NUMBER, preset.getChannelNumber());
-        values.put(RadioPresetsTable.Columns.SUB_CHANNEL, preset.getSubChannelNumber());
-        values.put(RadioPresetsTable.Columns.BAND, preset.getRadioBand());
-
-        if (preset.getRds() != null) {
-            values.put(RadioPresetsTable.Columns.PROGRAM_SERVICE,
-                    preset.getRds().getProgramService());
-        }
-
-        SQLiteDatabase db = getWritableDatabase();
-        long status;
-
-        db.beginTransaction();
-        try {
-            status = db.insertWithOnConflict(RadioPresetsTable.NAME, null /* nullColumnHack */,
-                    values, SQLiteDatabase.CONFLICT_REPLACE);
-
-            db.setTransactionSuccessful();
-        } finally {
-            db.endTransaction();
-            db.close();
-        }
-
-        return status != -1;
-    }
-
-    /**
-     * Removes the preset represented by the given {@link RadioStation}.
-     *
-     * @return {@code true} if the operation succeeded.
-     */
-    @WorkerThread
-    public boolean deletePreset(RadioStation preset) {
-        assertNotMainThread();
-
-        SQLiteDatabase db = getWritableDatabase();
-        long rowsDeleted;
-
-        db.beginTransaction();
-        try {
-            String channelNumber = Integer.toString(preset.getChannelNumber());
-            String subChannelNumber = Integer.toString(preset.getSubChannelNumber());
-
-            rowsDeleted = db.delete(RadioPresetsTable.NAME, DELETE_PRESETS_WHERE_CLAUSE,
-                    new String[] { channelNumber, subChannelNumber });
-
-            db.setTransactionSuccessful();
-        } finally {
-            db.endTransaction();
-            db.close();
-        }
-
-        return rowsDeleted != 0;
-    }
-
-    /**
-     * Returns all the pre-scanned stations for the given radio band.
-     *
-     * @param radioBand One of the band values in {@link android.hardware.radio.RadioManager}.
-     * @return A list of pre-scanned stations or an empty array if no stations found.
-     */
-    @NonNull
-    @WorkerThread
-    public List<RadioStation> getAllPreScannedStationsForBand(int radioBand) {
-        assertNotMainThread();
-
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "getAllPreScannedStationsForBand()");
-        }
-
-        SQLiteDatabase db = getReadableDatabase();
-        List<RadioStation> stations = new ArrayList<>();
-        Cursor cursor = null;
-
-        db.beginTransaction();
-        try {
-            cursor = db.rawQuery(GET_ALL_PRE_SCAN_FOR_BAND,
-                    new String[] { Integer.toString(radioBand) });
-
-            while (cursor.moveToNext()) {
-                int channel = cursor.getInt(cursor.getColumnIndexOrThrow(
-                        PreScannedStationsTable.Columns.CHANNEL_NUMBER));
-                int subChannel = cursor.getInt(
-                        cursor.getColumnIndexOrThrow(PreScannedStationsTable.Columns.SUB_CHANNEL));
-                int band = cursor.getInt(
-                        cursor.getColumnIndexOrThrow(PreScannedStationsTable.Columns.BAND));
-                String programService = cursor.getString(cursor.getColumnIndexOrThrow(
-                        PreScannedStationsTable.Columns.PROGRAM_SERVICE));
-
-                RadioRds rds = null;
-                if (!TextUtils.isEmpty(programService)) {
-                    rds = new RadioRds(programService, null /* songArtist */, null /* songTitle */);
-                }
-
-                stations.add(new RadioStation(channel, subChannel, band, rds));
-            }
-
-            db.setTransactionSuccessful();
-        } finally {
-            if (cursor != null) {
-                cursor.close();
-            }
-
-            db.endTransaction();
-            db.close();
-        }
-
-        return stations;
-    }
-
-    /**
-     * Inserts the given list of {@link RadioStation}s as the list of pre-scanned stations for the
-     * given band. This operation will clear all currently stored stations for the given band
-     * and replace them with the given list.
-     *
-     * @param radioBand One of the band values in {@link android.hardware.radio.RadioManager}.
-     * @param stations A list of {@link RadioStation}s representing the pre-scanned stations.
-     * @return {@code true} if the operation was successful.
-     */
-    @WorkerThread
-    public boolean insertPreScannedStations(int radioBand, List<RadioStation> stations) {
-        assertNotMainThread();
-
-        SQLiteDatabase db = getWritableDatabase();
-        db.beginTransaction();
-
-        long status = -1;
-
-        try {
-            // First clear all pre-scanned stations for the given radio band so that they can be
-            // replaced by the list of stations.
-            db.delete(PreScannedStationsTable.NAME, DELETE_PRE_SCAN_WHERE_CLAUSE,
-                    new String[] { Integer.toString(radioBand) });
-
-            for (RadioStation station : stations) {
-                ContentValues values = new ContentValues();
-                values.put(PreScannedStationsTable.Columns.CHANNEL_NUMBER,
-                        station.getChannelNumber());
-                values.put(PreScannedStationsTable.Columns.SUB_CHANNEL,
-                        station.getSubChannelNumber());
-                values.put(PreScannedStationsTable.Columns.BAND, station.getRadioBand());
-
-                if (station.getRds() != null) {
-                    values.put(PreScannedStationsTable.Columns.PROGRAM_SERVICE,
-                            station.getRds().getProgramService());
-                }
-
-                status = db.insertWithOnConflict(PreScannedStationsTable.NAME,
-                        null /* nullColumnHack */, values, SQLiteDatabase.CONFLICT_REPLACE);
-            }
-
-            db.setTransactionSuccessful();
-        } finally {
-            db.endTransaction();
-            db.close();
-        }
-
-        return status != -1;
-    }
-
-    /**
-     * Checks that the current thread is not the main thread. If it is, then an
-     * {@link IllegalStateException} is thrown. This assert should be called before all database
-     * operations.
-     */
-    private void assertNotMainThread() {
-        if (Looper.myLooper() == Looper.getMainLooper()) {
-            throw new IllegalStateException("Attempting to call database methods on main thread.");
-        }
-    }
-
-    @Override
-    public void onCreate(SQLiteDatabase db) {
-        db.execSQL(CREATE_PRESETS_TABLE);
-        db.execSQL(CREATE_PRE_SCAN_TABLE);
-    }
-
-    @Override
-    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
-        db.beginTransaction();
-
-        try {
-            // Currently no upgrade steps as this is the first version of the database. Simply drop
-            // all tables and re-create.
-            db.execSQL(DELETE_PRESETS_TABLE);
-            db.execSQL(DELETE_PRE_SCAN_TABLE);
-            onCreate(db);
-
-            db.setTransactionSuccessful();
-        } finally {
-            db.endTransaction();
-        }
-    }
-
-    @Override
-    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
-        onUpgrade(db, oldVersion, newVersion);
-    }
-}
diff --git a/src/com/android/car/radio/RadioDisplayController.java b/src/com/android/car/radio/RadioDisplayController.java
index 8bb9577..f9b1b7e 100644
--- a/src/com/android/car/radio/RadioDisplayController.java
+++ b/src/com/android/car/radio/RadioDisplayController.java
@@ -17,7 +17,6 @@
 package com.android.car.radio;
 
 import android.content.Context;
-import android.content.res.Resources;
 import android.media.session.PlaybackState;
 import android.text.TextUtils;
 import android.view.View;
@@ -34,10 +33,8 @@
     private TextView mChannelBand;
     private TextView mChannelNumber;
 
-    private CarouselView mChannelList;
-
-    private TextView mCurrentSongTitle;
-    private TextView mCurrentSongArtistOrStation;
+    private TextView mCurrentSongTitleAndArtist;
+    private TextView mCurrentStation;
 
     private ImageView mBackwardSeekButton;
     private ImageView mForwardSeekButton;
@@ -58,8 +55,8 @@
         mChannelBand = container.findViewById(R.id.radio_station_band);
         mChannelNumber = container.findViewById(R.id.radio_station_channel);
 
-        mCurrentSongTitle = container.findViewById(R.id.radio_station_song);
-        mCurrentSongArtistOrStation = container.findViewById(R.id.radio_station_artist_or_station);
+        mCurrentSongTitleAndArtist = container.findViewById(R.id.radio_station_song_artist);
+        mCurrentStation = container.findViewById(R.id.radio_station_name);
 
         mBackwardSeekButton = container.findViewById(R.id.radio_back_button);
         mForwardSeekButton = container.findViewById(R.id.radio_forward_button);
@@ -88,38 +85,6 @@
     }
 
     /**
-     * Sets this controller to display a list of channels that include the current radio station as
-     * well as pre-scanned stations for the current band.
-     */
-    public void setChannelListDisplay(View container, PrescannedRadioStationAdapter adapter) {
-        ViewStub stub = container.findViewById(R.id.channel_list_view_stub);
-
-        if (stub == null) {
-            return;
-        }
-
-        mChannelList = (CarouselView) stub.inflate();
-        mChannelList.setAdapter(adapter);
-
-        Resources res = mContext.getResources();
-        int topOffset = res.getDimensionPixelSize(R.dimen.lens_header_height)
-                + res.getDimensionPixelSize(R.dimen.car_radio_container_top_padding)
-                + res.getDimensionPixelSize(R.dimen.car_radio_station_top_margin);
-
-        mChannelList.setTopOffset(topOffset);
-    }
-
-    /**
-     * Set the given position as the radio station that should be be displayed first in the channel
-     * list controlled by this class.
-     */
-    public void setCurrentStationInList(int position) {
-        if (mChannelList != null) {
-            mChannelList.shiftToPosition(position);
-        }
-    }
-
-    /**
      * Set whether or not the buttons controlled by this controller are enabled. If {@code false}
      * is passed to this method, then no {@link View.OnClickListener}s will be
      * triggered when the buttons are pressed. In addition, the look of the button wil be updated
@@ -229,11 +194,22 @@
     /**
      * Sets the title of the currently playing song.
      */
-    public void setCurrentSongTitle(String songTitle) {
-        if (mCurrentSongTitle != null) {
-            boolean isEmpty = TextUtils.isEmpty(songTitle);
-            mCurrentSongTitle.setText(isEmpty ? null : songTitle.trim());
-            mCurrentSongTitle.setVisibility(isEmpty ? View.GONE : View.VISIBLE);
+    public void setCurrentSongTitleAndArtist(String songTitle, String songArtist) {
+        if (mCurrentSongTitleAndArtist != null) {
+            boolean isTitleEmpty = TextUtils.isEmpty(songTitle);
+            boolean isArtistEmpty = TextUtils.isEmpty(songArtist);
+            String titleAndArtist = null;
+            if (!isTitleEmpty) {
+                titleAndArtist = songTitle.trim();
+                if (!isArtistEmpty) {
+                    titleAndArtist += '\u2014' + songArtist.trim();
+                }
+            } else if (!isArtistEmpty) {
+                titleAndArtist = songArtist.trim();
+            }
+            mCurrentSongTitleAndArtist.setText(titleAndArtist);
+            mCurrentSongTitleAndArtist.setVisibility(
+                    (isTitleEmpty && isArtistEmpty)? View.INVISIBLE : View.VISIBLE);
         }
     }
 
@@ -241,11 +217,11 @@
      * Sets the artist(s) of the currently playing song or current radio station information
      * (e.g. KOIT).
      */
-    public void setCurrentSongArtistOrStation(String songArtist) {
-        if (mCurrentSongArtistOrStation != null) {
-            boolean isEmpty = TextUtils.isEmpty(songArtist);
-            mCurrentSongArtistOrStation.setText(isEmpty ? null : songArtist.trim());
-            mCurrentSongArtistOrStation.setVisibility(isEmpty ? View.GONE : View.VISIBLE);
+    public void setCurrentStation(String stationName) {
+        if (mCurrentStation != null) {
+            boolean isEmpty = TextUtils.isEmpty(stationName);
+            mCurrentStation.setText(isEmpty ? null : stationName.trim());
+            mCurrentStation.setVisibility(isEmpty ? View.INVISIBLE : View.VISIBLE);
         }
     }
 
diff --git a/src/com/android/car/radio/RadioFabButton.java b/src/com/android/car/radio/RadioFabButton.java
index 2430de7..94e5c8b 100644
--- a/src/com/android/car/radio/RadioFabButton.java
+++ b/src/com/android/car/radio/RadioFabButton.java
@@ -15,10 +15,11 @@
  */
 package com.android.car.radio;
 
+import android.annotation.ColorInt;
 import android.content.Context;
-import android.support.annotation.ColorInt;
 import android.util.AttributeSet;
 import android.widget.ImageView;
+
 import com.android.car.apps.common.FabDrawable;
 
 /**
diff --git a/src/com/android/car/radio/RadioPresetsFragment.java b/src/com/android/car/radio/RadioPresetsFragment.java
index 44d5000..a022c4a 100644
--- a/src/com/android/car/radio/RadioPresetsFragment.java
+++ b/src/com/android/car/radio/RadioPresetsFragment.java
@@ -19,7 +19,9 @@
 import android.animation.Animator;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
+import android.annotation.NonNull;
 import android.content.Context;
+import android.hardware.radio.RadioManager.ProgramInfo;
 import android.os.Bundle;
 import android.support.v4.app.Fragment;
 import android.util.Log;
@@ -27,8 +29,11 @@
 import android.view.View;
 import android.view.ViewGroup;
 
-import com.android.car.radio.service.RadioStation;
-import com.android.car.view.PagedListView;
+import androidx.car.widget.DayNightStyle;
+import androidx.car.widget.PagedListView;
+
+import com.android.car.broadcastradio.support.Program;
+import com.android.car.radio.storage.RadioStorage;
 
 import java.util.List;
 
@@ -37,14 +42,12 @@
  */
 public class RadioPresetsFragment extends Fragment implements
         FragmentWithFade,
-        PresetsAdapter.OnPresetItemClickListener,
         RadioAnimationManager.OnExitCompleteListener,
-        RadioController.RadioStationChangeListener,
+        RadioController.ProgramInfoChangeListener,
         RadioStorage.PresetsChangeListener {
     private static final String TAG = "PresetsFragment";
     private static final int ANIM_DURATION_MS = 200;
 
-    private View mRootView;
     private View mCurrentRadioCard;
 
     private RadioStorage mRadioStorage;
@@ -79,34 +82,35 @@
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedInstanceState) {
-        mRootView = inflater.inflate(R.layout.radio_presets_list, container, false);
+        return inflater.inflate(R.layout.radio_presets_list, container, false);
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
         Context context = getContext();
 
-        mPresetsAdapter.setOnPresetItemClickListener(this);
+        mPresetsAdapter.setOnPresetItemClickListener(mRadioController::tune);
+        mPresetsAdapter.setOnPresetItemFavoriteListener(this::handlePresetItemFavoriteChanged);
 
-        mCurrentRadioCard = mRootView.findViewById(R.id.current_radio_station_card);
+        mCurrentRadioCard = view.findViewById(R.id.current_radio_station_card);
 
-        mAnimManager = new RadioAnimationManager(getContext(), mRootView);
+        mAnimManager = new RadioAnimationManager(getContext(), view);
         mAnimManager.playEnterAnimation();
 
         // Clicking on the current radio station card will close the activity and return to the
         // main radio screen.
-        mCurrentRadioCard.setOnClickListener(v -> {
-            mAnimManager.playExitAnimation(RadioPresetsFragment.this /* listener */);
-        });
+        mCurrentRadioCard.setOnClickListener(
+                v -> mAnimManager.playExitAnimation(RadioPresetsFragment.this /* listener */));
 
-        mPresetsList = mRootView.findViewById(R.id.presets_list);
-        mPresetsList.setLightMode();
+        mPresetsList = view.findViewById(R.id.presets_list);
+        mPresetsList.setDayNightStyle(DayNightStyle.ALWAYS_LIGHT);
         mPresetsList.setAdapter(mPresetsAdapter);
-        mPresetsList.getLayoutManager().setOffsetRows(false);
         mPresetsList.getRecyclerView().addOnScrollListener(new PresetListScrollListener(
-                context, mRootView, mCurrentRadioCard, mPresetsList));
+                context, view, mCurrentRadioCard, mPresetsList));
 
         mRadioStorage = RadioStorage.getInstance(context);
         mRadioStorage.addPresetsChangeListener(this);
         setPresetsOnList(mRadioStorage.getPresets());
-
-        return mRootView;
     }
 
     @Override
@@ -204,16 +208,21 @@
     @Override
     public void onStart() {
         super.onStart();
-        mRadioController.initialize(mRootView);
+        mRadioController.initialize(getView());
         mRadioController.setShouldColorStatusBar(true);
-        mRadioController.setRadioStationChangeListener(this);
+        mRadioController.addProgramInfoChangeListener(this);
 
-        mPresetsAdapter.setActiveRadioStation(mRadioController.getCurrentRadioStation());
+        // TODO(b/73950974): use callback only
+        ProgramInfo info = mRadioController.getCurrentProgramInfo();
+        if (info != null) {
+            mPresetsAdapter.setActiveProgram(Program.fromProgramInfo(info));
+        }
     }
 
     @Override
     public void onDestroy() {
         mRadioStorage.removePresetsChangeListener(this);
+        mRadioController.removeProgramInfoChangeListener(this);
         mPresetListExitListener = null;
         super.onDestroy();
     }
@@ -225,18 +234,11 @@
         }
     }
 
-    @Override
-    public void onPresetItemClicked(RadioStation station) {
-        mRadioController.tuneToRadioChannel(station);
-    }
 
     @Override
-    public void onRadioStationChanged(RadioStation station) {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "onRadioStationChanged(): " + station);
-        }
-
-        mPresetsAdapter.setActiveRadioStation(station);
+    public void onProgramInfoChanged(@NonNull ProgramInfo info) {
+        mPresetsAdapter.setPresets(mRadioStorage.getPresets());
+        mPresetsAdapter.setActiveProgram(Program.fromProgramInfo(info));
     }
 
     @Override
@@ -244,24 +246,24 @@
         if (Log.isLoggable(TAG, Log.DEBUG)) {
             Log.d(TAG, "onPresetsRefreshed()");
         }
+    }
 
-        setPresetsOnList(mRadioStorage.getPresets());
+    private void handlePresetItemFavoriteChanged(Program program, boolean saveAsFavorite) {
+        if (saveAsFavorite) {
+            mRadioStorage.storePreset(program);
+        } else {
+            mRadioStorage.removePreset(program.getSelector());
+        }
     }
 
     /**
      * Sets the given list of presets into the PagedListView {@link #mPresetsList}.
      */
-    private void setPresetsOnList(List<RadioStation> presets) {
+    private void setPresetsOnList(List<Program> presets) {
         if (Log.isLoggable(TAG, Log.DEBUG)) {
             Log.d(TAG, "setPresetsOnList(). # of presets: " + presets.size());
         }
 
-        if (Log.isLoggable(TAG, Log.VERBOSE)) {
-            for (RadioStation radioStation : presets) {
-                Log.v(TAG, "\t" + radioStation);
-            }
-        }
-
         mPresetsAdapter.setPresets(presets);
     }
 
diff --git a/src/com/android/car/radio/RadioService.java b/src/com/android/car/radio/RadioService.java
index 9e35faa..fc5a9ad 100644
--- a/src/com/android/car/radio/RadioService.java
+++ b/src/com/android/car/radio/RadioService.java
@@ -17,34 +17,38 @@
 package com.android.car.radio;
 
 import android.app.Service;
-import android.car.hardware.radio.CarRadioManager;
-import android.content.Context;
 import android.content.Intent;
-import android.content.pm.PackageManager;
+import android.hardware.radio.ProgramList;
+import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
-import android.hardware.radio.RadioMetadata;
+import android.hardware.radio.RadioManager.ProgramInfo;
 import android.hardware.radio.RadioTuner;
-import android.media.AudioAttributes;
-import android.media.AudioManager;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
-import android.os.SystemProperties;
-import android.support.annotation.Nullable;
-import android.support.car.Car;
-import android.support.car.CarNotConnectedException;
-import android.support.car.CarConnectionCallback;
-import android.support.car.media.CarAudioManager;
-import android.text.TextUtils;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
+import android.support.v4.media.MediaBrowserServiceCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
 import android.util.Log;
-import com.android.car.radio.demo.RadioDemo;
+
+import com.android.car.broadcastradio.support.Program;
+import com.android.car.broadcastradio.support.media.BrowseTree;
+import com.android.car.broadcastradio.support.platform.ProgramSelectorExt;
+import com.android.car.radio.audio.AudioStreamController;
+import com.android.car.radio.audio.IPlaybackStateListener;
+import com.android.car.radio.media.TunerSession;
+import com.android.car.radio.platform.ImageMemoryCache;
+import com.android.car.radio.platform.RadioManagerExt;
 import com.android.car.radio.service.IRadioCallback;
 import com.android.car.radio.service.IRadioManager;
-import com.android.car.radio.service.RadioRds;
-import com.android.car.radio.service.RadioStation;
+import com.android.car.radio.storage.RadioStorage;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
 
 /**
  * A persistent {@link Service} that is responsible for opening and closing a {@link RadioTuner}.
@@ -53,40 +57,39 @@
  *
  * <p>Utilize the {@link RadioBinder} to perform radio operations.
  */
-public class RadioService extends Service implements AudioManager.OnAudioFocusChangeListener {
-    private static String TAG = "Em.RadioService";
+public class RadioService extends MediaBrowserServiceCompat implements IPlaybackStateListener {
+
+    private static String TAG = "BcRadioApp.uisrv";
+
+    public static String ACTION_UI_SERVICE = "com.android.car.radio.ACTION_UI_SERVICE";
 
     /**
      * The amount of time to wait before re-trying to open the {@link #mRadioTuner}.
      */
     private static final int RADIO_TUNER_REOPEN_DELAY_MS = 5000;
 
+    private final Object mLock = new Object();
+
     private int mReOpenRadioTunerCount = 0;
     private final Handler mHandler = new Handler();
 
-    private Car mCarApi;
+    private RadioStorage mRadioStorage;
+    private final RadioStorage.PresetsChangeListener mPresetsListener = this::onPresetsChanged;
+
     private RadioTuner mRadioTuner;
 
     private boolean mRadioSuccessfullyInitialized;
-    private int mCurrentRadioBand = RadioManager.BAND_FM;
-    private int mCurrentRadioChannel = RadioStorage.INVALID_RADIO_CHANNEL;
 
-    private String mCurrentChannelInfo;
-    private String mCurrentArtist;
-    private String mCurrentSongTitle;
+    private ProgramInfo mCurrentProgram;
 
-    private RadioManager mRadioManager;
-    private RadioBackgroundScanner mBackgroundScanner;
-    private RadioManager.FmBandDescriptor mFmDescriptor;
-    private RadioManager.AmBandDescriptor mAmDescriptor;
+    private RadioManagerExt mRadioManager;
+    private ImageMemoryCache mImageCache;
 
-    private RadioManager.FmBandConfig mFmConfig;
-    private RadioManager.AmBandConfig mAmConfig;
+    private AudioStreamController mAudioStreamController;
 
-    private final List<RadioManager.ModuleProperties> mModules = new ArrayList<>();
-
-    private CarAudioManager mCarAudioManager;
-    private AudioAttributes mRadioAudioAttributes;
+    private BrowseTree mBrowseTree;
+    private TunerSession mMediaSession;
+    private ProgramList mProgramList;
 
     /**
      * Whether or not this {@link RadioService} currently has audio focus, meaning it is the
@@ -106,10 +109,10 @@
 
     @Override
     public IBinder onBind(Intent intent) {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "onBind(); Intent: " + intent);
+        if (ACTION_UI_SERVICE.equals(intent.getAction())) {
+            return mBinder;
         }
-        return mBinder;
+        return super.onBind(intent);
     }
 
     @Override
@@ -120,96 +123,23 @@
             Log.d(TAG, "onCreate()");
         }
 
-        // Connection to car services does not work for non-automotive yet, so this call needs to
-        // be guarded.
-        if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
-            mCarApi = Car.createCar(this /* context */, mCarConnectionCallback);
-            mCarApi.connect();
-        }
+        mRadioManager = new RadioManagerExt(this);
+        mAudioStreamController = new AudioStreamController(this, mRadioManager);
+        mRadioStorage = RadioStorage.getInstance(this);
+        mImageCache = new ImageMemoryCache(mRadioManager, 1000);
 
-        if (SystemProperties.getBoolean(RadioDemo.DEMO_MODE_PROPERTY, false)) {
-            initializeDemo();
-        } else {
-            initialze();
-        }
-    }
+        mBrowseTree = new BrowseTree(this, mImageCache);
+        mMediaSession = new TunerSession(this, mBrowseTree, mBinder, mImageCache);
+        setSessionToken(mMediaSession.getSessionToken());
+        mAudioStreamController.addPlaybackStateListener(mMediaSession);
+        mBrowseTree.setAmFmRegionConfig(mRadioManager.getAmFmRegionConfig());
 
-    /**
-     * Initializes this service to use a demo {@link IRadioManager}.
-     *
-     * @see RadioDemo
-     */
-    private void initializeDemo() {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "initializeDemo()");
-        }
+        mRadioStorage.addPresetsChangeListener(mPresetsListener);
+        onPresetsChanged();
 
-        mBinder = RadioDemo.getInstance(this /* context */).createDemoManager();
-    }
+        mAudioStreamController.addPlaybackStateListener(this);
 
-    /**
-     * Connects to the {@link RadioManager}.
-     */
-    private void initialze() {
-        mRadioManager = (RadioManager) getSystemService(Context.RADIO_SERVICE);
-
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "initialze(); mRadioManager: " + mRadioManager);
-        }
-
-        if (mRadioManager == null) {
-            Log.w(TAG, "RadioManager could not be loaded.");
-            return;
-        }
-
-        int status = mRadioManager.listModules(mModules);
-        if (status != RadioManager.STATUS_OK) {
-            Log.w(TAG, "Load modules failed with status: " + status);
-            return;
-        }
-
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "initialze(); listModules complete: " + mModules);
-        }
-
-        if (mModules.size() == 0) {
-            Log.w(TAG, "No radio modules on device.");
-            return;
-        }
-
-        boolean isDebugLoggable = Log.isLoggable(TAG, Log.DEBUG);
-
-        // Load the possible radio bands. For now, just accept FM and AM bands.
-        for (RadioManager.BandDescriptor band : mModules.get(0).getBands()) {
-            if (isDebugLoggable) {
-                Log.d(TAG, "loading band: " + band.toString());
-            }
-
-            if (mFmDescriptor == null && band.isFmBand()) {
-                mFmDescriptor = (RadioManager.FmBandDescriptor) band;
-            }
-
-            if (mAmDescriptor == null && band.isAmBand()) {
-                mAmDescriptor = (RadioManager.AmBandDescriptor) band;
-            }
-        }
-
-        if (mFmDescriptor == null && mAmDescriptor == null) {
-            Log.w(TAG, "No AM and FM radio bands could be loaded.");
-            return;
-        }
-
-        // TODO: Make stereo configurable depending on device.
-        mFmConfig = new RadioManager.FmBandConfig.Builder(mFmDescriptor)
-                .setStereo(true)
-                .build();
-        mAmConfig = new RadioManager.AmBandConfig.Builder(mAmDescriptor)
-                .setStereo(true)
-                .build();
-
-        // If there is a second tuner on the device, then set it up as the background scanner.
-        // TODO(b/63101896): we don't know if the second tuner is for the same medium, so we don't
-        // set background scanner for now.
+        openRadioBandInternal(mRadioStorage.getStoredRadioBand());
 
         mRadioSuccessfullyInitialized = true;
     }
@@ -220,15 +150,21 @@
             Log.d(TAG, "onDestroy()");
         }
 
+        mRadioStorage.removePresetsChangeListener(mPresetsListener);
+        mMediaSession.release();
+        mRadioManager.getRadioTunerExt().close();
         close();
 
-        if (mCarApi != null) {
-            mCarApi.disconnect();
-        }
-
         super.onDestroy();
     }
 
+    private void onPresetsChanged() {
+        synchronized (mLock) {
+            mBrowseTree.setFavorites(new HashSet<>(mRadioStorage.getPresets()));
+            mMediaSession.notifyFavoritesChanged();
+        }
+    }
+
     /**
      * Opens the current radio band. Currently, this only supports FM and AM bands.
      *
@@ -238,159 +174,62 @@
      * {@link RadioManager#STATUS_ERROR}.
      */
     private int openRadioBandInternal(int radioBand) {
-        if (requestAudioFocus() != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
-            Log.e(TAG, "openRadioBandInternal() audio focus request fail");
-            return RadioManager.STATUS_ERROR;
-        }
+        if (!mAudioStreamController.requestMuted(false)) return RadioManager.STATUS_ERROR;
 
-        mCurrentRadioBand = radioBand;
-        RadioManager.BandConfig config = getRadioConfig(radioBand);
-
-        if (config == null) {
-            Log.w(TAG, "Cannot create config for radio band: " + radioBand);
-            return RadioManager.STATUS_ERROR;
-        }
-
-        if (mRadioTuner != null) {
-            mRadioTuner.setConfiguration(config);
-        } else {
-            mRadioTuner = mRadioManager.openTuner(mModules.get(0).getId(), config, true,
-                    mInternalRadioTunerCallback, null /* handler */);
+        if (mRadioTuner == null) {
+            mRadioTuner = mRadioManager.openSession(mInternalRadioTunerCallback, null);
+            mProgramList = mRadioTuner.getDynamicProgramList(null);
+            mBrowseTree.setProgramList(mProgramList);
         }
 
         if (Log.isLoggable(TAG, Log.DEBUG)) {
             Log.d(TAG, "openRadioBandInternal() STATUS_OK");
         }
 
-        if (mBackgroundScanner != null) {
-            mBackgroundScanner.onRadioBandChanged(radioBand);
-        }
-
         // Reset the counter for exponential backoff each time the radio tuner has been successfully
         // opened.
         mReOpenRadioTunerCount = 0;
 
+        tuneToDefault(radioBand);
+
         return RadioManager.STATUS_OK;
     }
 
-    /**
-     * Returns a {@link RadioRds} object that holds all the current radio metadata. If all the
-     * metadata is empty, then {@code null} is returned.
-     */
-    @Nullable
-    private RadioRds createCurrentRadioRds() {
-        if (TextUtils.isEmpty(mCurrentChannelInfo) && TextUtils.isEmpty(mCurrentArtist)
-                && TextUtils.isEmpty(mCurrentSongTitle)) {
-            return null;
-        }
+    private void tuneToDefault(int band) {
+        if (!mAudioStreamController.preparePlayback(Optional.empty())) return;
 
-        return new RadioRds(mCurrentChannelInfo, mCurrentArtist, mCurrentSongTitle);
-    }
+        long storedChannel = mRadioStorage.getStoredRadioChannel(band);
+        if (storedChannel != RadioStorage.INVALID_RADIO_CHANNEL) {
+            Log.i(TAG, "Restoring stored program: " + storedChannel);
+            mRadioTuner.tune(ProgramSelectorExt.createAmFmSelector(storedChannel));
+        } else {
+            Log.i(TAG, "No stored program, seeking forward to not play static");
 
-    /**
-     * Creates a {@link RadioStation} that encapsulates all the information about the current
-     * radio station.
-     */
-    private RadioStation createCurrentRadioStation() {
-        // mCurrentRadioChannel can possibly be invalid if this class never receives a callback
-        // for onProgramInfoChanged(). As a result, manually retrieve the information for the
-        // current station from RadioTuner if this is the case.
-        if (mCurrentRadioChannel == RadioStorage.INVALID_RADIO_CHANNEL && mRadioTuner != null) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "createCurrentRadioStation(); invalid current radio channel. "
-                        + "Calling getProgramInformation for valid station");
-            }
+            // TODO(b/80500464): don't hardcode, pull from tuner config
+            long lastChannel;
+            if (band == RadioManager.BAND_AM) lastChannel = 1620;
+            else lastChannel = 108000;
+            mRadioTuner.tune(ProgramSelectorExt.createAmFmSelector(lastChannel));
 
-            // getProgramInformation() expects an array of size 1.
-            RadioManager.ProgramInfo[] info = new RadioManager.ProgramInfo[1];
-            int status = mRadioTuner.getProgramInformation(info);
-
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "getProgramInformation() status: " + status + "; info: " + info[0]);
-            }
-
-            if (status == RadioManager.STATUS_OK && info[0] != null) {
-                mCurrentRadioChannel = info[0].getChannel();
-
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "program info channel: " + mCurrentRadioChannel);
-                }
-            }
-        }
-
-        return new RadioStation(mCurrentRadioChannel, 0 /* subChannelNumber */,
-                mCurrentRadioBand, createCurrentRadioRds());
-    }
-
-    /**
-     * Returns the proper {@link android.hardware.radio.RadioManager.BandConfig} for the given
-     * radio band. {@code null} is returned if the band is not suppored.
-     */
-    @Nullable
-    private RadioManager.BandConfig getRadioConfig(int selectedRadioBand) {
-        switch (selectedRadioBand) {
-            case RadioManager.BAND_AM:
-            case RadioManager.BAND_AM_HD:
-                return mAmConfig;
-            case RadioManager.BAND_FM:
-            case RadioManager.BAND_FM_HD:
-                return mFmConfig;
-
-            default:
-                return null;
+            mRadioTuner.scan(RadioTuner.DIRECTION_UP, true);
         }
     }
 
-    private int requestAudioFocus() {
-        int status = AudioManager.AUDIOFOCUS_REQUEST_FAILED;
-        try {
-            status = mCarAudioManager.requestAudioFocus(this, mRadioAudioAttributes,
-                    AudioManager.AUDIOFOCUS_GAIN, 0);
-        } catch (CarNotConnectedException e) {
-            Log.e(TAG, "requestAudioFocus() failed", e);
-        }
-
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "requestAudioFocus status: " + status);
-        }
-
-        if (status == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
-            mHasAudioFocus = true;
-
-            // Receiving audio focus means that the radio is un-muted.
+    /* TODO(b/73950974): remove onRadioMuteChanged from IRadioCallback,
+     * use IPlaybackStateListener directly.
+     */
+    @Override
+    public void onPlaybackStateChanged(@PlaybackStateCompat.State int state) {
+        boolean muted = state != PlaybackStateCompat.STATE_PLAYING;
+        synchronized (mLock) {
             for (IRadioCallback callback : mRadioTunerCallbacks) {
                 try {
-                    callback.onRadioMuteChanged(false);
+                    callback.onRadioMuteChanged(muted);
                 } catch (RemoteException e) {
-                    Log.e(TAG, "requestAudioFocus(); onRadioMuteChanged() notify failed: "
-                            + e.getMessage());
+                    Log.e(TAG, "Mute state change callback failed", e);
                 }
             }
         }
-
-        return status;
-    }
-
-    private void abandonAudioFocus() {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "abandonAudioFocus()");
-        }
-
-        if (mCarAudioManager == null) {
-            return;
-        }
-
-        mCarAudioManager.abandonAudioFocus(this, mRadioAudioAttributes);
-        mHasAudioFocus = false;
-
-        for (IRadioCallback callback : mRadioTunerCallbacks) {
-            try {
-                callback.onRadioMuteChanged(true);
-            } catch (RemoteException e) {
-                Log.e(TAG, "abandonAudioFocus(); onRadioMutechanged() notify failed: "
-                        + e.getMessage());
-            }
-        }
     }
 
     /**
@@ -401,100 +240,32 @@
             Log.d(TAG, "close()");
         }
 
-        abandonAudioFocus();
+        mAudioStreamController.requestMuted(true);
 
+        if (mProgramList != null) {
+            mProgramList.close();
+            mProgramList = null;
+        }
         if (mRadioTuner != null) {
             mRadioTuner.close();
             mRadioTuner = null;
         }
     }
 
-    @Override
-    public void onAudioFocusChange(int focusChange) {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "focus change: " + focusChange);
-        }
-
-        switch (focusChange) {
-            case AudioManager.AUDIOFOCUS_GAIN:
-                mHasAudioFocus = true;
-                openRadioBandInternal(mCurrentRadioBand);
-                break;
-
-            // For a transient loss, just allow the focus to be released. The radio will stop
-            // itself automatically. There is no need for an explicit abandon audio focus call
-            // because this removes the AudioFocusChangeListener.
-            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
-            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
-                mHasAudioFocus = false;
-                break;
-
-            case AudioManager.AUDIOFOCUS_LOSS:
-                close();
-                break;
-
-            default:
-                // Do nothing for all other cases.
-        }
-    }
-
-    /**
-     * {@link CarConnectionCallback} that retrieves the {@link CarRadioManager}.
-     */
-    private final CarConnectionCallback mCarConnectionCallback =
-            new CarConnectionCallback() {
-                @Override
-                public void onConnected(Car car) {
-                    if (Log.isLoggable(TAG, Log.DEBUG)) {
-                        Log.d(TAG, "Car service connected.");
-                    }
-                    try {
-                        // The CarAudioManager only needs to be retrieved once.
-                        if (mCarAudioManager == null) {
-                            mCarAudioManager = (CarAudioManager) mCarApi.getCarManager(
-                                    android.car.Car.AUDIO_SERVICE);
-
-                            mRadioAudioAttributes = mCarAudioManager.getAudioAttributesForCarUsage(
-                                    CarAudioManager.CAR_AUDIO_USAGE_RADIO);
-                        }
-                    } catch (CarNotConnectedException e) {
-                        //TODO finish
-                        Log.e(TAG, "Car not connected");
-                    }
-                }
-
-                @Override
-                public void onDisconnected(Car car) {
-                    if (Log.isLoggable(TAG, Log.DEBUG)) {
-                        Log.d(TAG, "Car service disconnected.");
-                    }
-                }
-            };
-
     private IRadioManager.Stub mBinder = new IRadioManager.Stub() {
         /**
          * Tunes the radio to the given frequency. To be notified of a successful tune, register
          * as a {@link android.hardware.radio.RadioTuner.Callback}.
          */
         @Override
-        public void tune(RadioStation radioStation) {
-            if (mRadioManager == null || radioStation == null
-                    || requestAudioFocus() != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
-                return;
-            }
+        public void tune(ProgramSelector sel) {
+            if (!mAudioStreamController.preparePlayback(Optional.empty())) return;
+            mRadioTuner.tune(sel);
+        }
 
-            if (mRadioTuner == null || radioStation.getRadioBand() != mCurrentRadioBand) {
-                int radioStatus = openRadioBandInternal(radioStation.getRadioBand());
-                if (radioStatus == RadioManager.STATUS_ERROR) {
-                    return;
-                }
-            }
-
-            int status = mRadioTuner.tune(radioStation.getChannelNumber(), 0 /* subChannel */);
-
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "Tuning to station: " + radioStation + "\n\tstatus: " + status);
-            }
+        @Override
+        public List<ProgramInfo> getProgramList() {
+            return mRadioTuner.getDynamicProgramList(null).toList();
         }
 
         /**
@@ -503,13 +274,10 @@
          */
         @Override
         public void seekForward() {
-            if (mRadioManager == null
-                    || requestAudioFocus() != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
-                return;
-            }
+            if (!mAudioStreamController.preparePlayback(Optional.of(true))) return;
 
             if (mRadioTuner == null) {
-                int radioStatus = openRadioBandInternal(mCurrentRadioBand);
+                int radioStatus = openRadioBandInternal(mRadioStorage.getStoredRadioBand());
                 if (radioStatus == RadioManager.STATUS_ERROR) {
                     return;
                 }
@@ -524,13 +292,10 @@
          */
         @Override
         public void seekBackward() {
-            if (mRadioManager == null
-                    || requestAudioFocus() != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
-                return;
-            }
+            if (!mAudioStreamController.preparePlayback(Optional.of(false))) return;
 
             if (mRadioTuner == null) {
-                int radioStatus = openRadioBandInternal(mCurrentRadioBand);
+                int radioStatus = openRadioBandInternal(mRadioStorage.getStoredRadioBand());
                 if (radioStatus == RadioManager.STATUS_ERROR) {
                     return;
                 }
@@ -546,51 +311,7 @@
          */
         @Override
         public boolean mute() {
-            if (mRadioManager == null) {
-                return false;
-            }
-
-            if (mCarAudioManager == null) {
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "mute() called, but not connected to CarAudioManager");
-                }
-                return false;
-            }
-
-            // If the radio does not currently have focus, then no need to do anything because the
-            // radio won't be playing any sound.
-            if (!mHasAudioFocus) {
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "mute() called, but radio does not currently have audio focus; "
-                            + "ignoring.");
-                }
-                return false;
-            }
-
-            boolean muteSuccessful = false;
-
-            try {
-                muteSuccessful = mCarAudioManager.setMediaMute(true);
-
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "setMediaMute(true) status: " + muteSuccessful);
-                }
-            } catch (CarNotConnectedException e) {
-                Log.e(TAG, "mute() failed: " + e.getMessage());
-                e.printStackTrace();
-            }
-
-            if (muteSuccessful && mRadioTunerCallbacks.size() > 0) {
-                for (IRadioCallback callback : mRadioTunerCallbacks) {
-                    try {
-                        callback.onRadioMuteChanged(true);
-                    } catch (RemoteException e) {
-                        Log.e(TAG, "mute() notify failed: " + e.getMessage());
-                    }
-                }
-            }
-
-            return muteSuccessful;
+            return mAudioStreamController.requestMuted(true);
         }
 
         /**
@@ -600,19 +321,7 @@
          */
         @Override
         public boolean unMute() {
-            if (mRadioManager == null) {
-                return false;
-            }
-
-            if (mCarAudioManager == null) {
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "toggleMute() called, but not connected to CarAudioManager");
-                }
-                return false;
-            }
-
-            // Requesting audio focus will automatically un-mute the radio if it had been muted.
-            return requestAudioFocus() == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+            return mAudioStreamController.requestMuted(false);
         }
 
         /**
@@ -620,52 +329,22 @@
          */
         @Override
         public boolean isMuted() {
-            if (!mHasAudioFocus) {
-                return true;
-            }
-
-            if (mRadioManager == null) {
-                return true;
-            }
-
-            if (mCarAudioManager == null) {
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "isMuted() called, but not connected to CarAudioManager");
-                }
-                return true;
-            }
-
-            boolean isMuted = false;
-
-            try {
-                isMuted = mCarAudioManager.isMediaMuted();
-            } catch (CarNotConnectedException e) {
-                Log.e(TAG, "isMuted() failed: " + e.getMessage());
-                e.printStackTrace();
-            }
-
-            return isMuted;
+            return mAudioStreamController.isMuted();
         }
 
-        /**
-         * Opens the radio for the given band.
-         *
-         * @param radioBand One of {@link RadioManager#BAND_FM}, {@link RadioManager#BAND_AM},
-         *                  {@link RadioManager#BAND_FM_HD} or {@link RadioManager#BAND_AM_HD}.
-         * @return {@link RadioManager#STATUS_OK} if successful; otherwise,
-         * {@link RadioManager#STATUS_ERROR}.
-         */
         @Override
-        public int openRadioBand(int radioBand) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "openRadioBand() for band: " + radioBand);
-            }
+        public void addFavorite(Program program) {
+            mRadioStorage.storePreset(program);
+        }
 
-            if (mRadioManager == null) {
-                return RadioManager.STATUS_ERROR;
-            }
+        @Override
+        public void removeFavorite(ProgramSelector sel) {
+            mRadioStorage.removePreset(sel);
+        }
 
-            return openRadioBandInternal(radioBand);
+        @Override
+        public void switchBand(int radioBand) {
+            tuneToDefault(radioBand);
         }
 
         /**
@@ -694,13 +373,9 @@
             mRadioTunerCallbacks.remove(callback);
         }
 
-        /**
-         * Returns a {@link RadioStation} that encapsulates the information about the current
-         * station the radio is tuned to.
-         */
         @Override
-        public RadioStation getCurrentRadioStation() {
-            return createCurrentRadioStation();
+        public ProgramInfo getCurrentProgramInfo() {
+            return mCurrentProgram;
         }
 
         /**
@@ -721,15 +396,6 @@
         public boolean hasFocus() {
             return mHasAudioFocus;
         }
-
-        /**
-         * Returns {@code true} if the current radio module has dual tuners, meaning that a tuner
-         * is available to scan for stations in the background.
-         */
-        @Override
-        public boolean hasDualTuners() {
-            return mModules.size() >= 2;
-        }
     };
 
     /**
@@ -738,79 +404,23 @@
      */
     private class InternalRadioCallback extends RadioTuner.Callback {
         @Override
-        public void onProgramInfoChanged(RadioManager.ProgramInfo info) {
+        public void onProgramInfoChanged(ProgramInfo info) {
             if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "onProgramInfoChanged(); info: " + info);
+                Log.d(TAG, "Program info changed: " + info);
             }
 
-            clearMetadata();
+            mCurrentProgram = Objects.requireNonNull(info);
+            mMediaSession.notifyProgramInfoChanged(info);
+            mAudioStreamController.notifyProgramInfoChanged();
+            mRadioStorage.storeRadioChannel(info.getSelector());
 
-            if (info != null) {
-                mCurrentRadioChannel = info.getChannel();
-
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "onProgramInfoChanged(); info channel: " + mCurrentRadioChannel);
+            for (IRadioCallback callback : mRadioTunerCallbacks) {
+                try {
+                    callback.onCurrentProgramInfoChanged(info);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Failed to notify about changed radio station", e);
                 }
             }
-
-            RadioStation station = createCurrentRadioStation();
-
-            try {
-                for (IRadioCallback callback : mRadioTunerCallbacks) {
-                    callback.onRadioStationChanged(station);
-                }
-            } catch (RemoteException e) {
-                Log.e(TAG, "onProgramInfoChanged(); "
-                        + "Failed to notify IRadioCallbacks: " + e.getMessage());
-            }
-        }
-
-        @Override
-        public void onMetadataChanged(RadioMetadata metadata) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "onMetadataChanged(); metadata: " + metadata);
-            }
-
-            clearMetadata();
-            updateMetadata(metadata);
-
-            RadioRds radioRds = createCurrentRadioRds();
-
-            try {
-                for (IRadioCallback callback : mRadioTunerCallbacks) {
-                    callback.onRadioMetadataChanged(radioRds);
-                }
-            } catch (RemoteException e) {
-                Log.e(TAG, "onMetadataChanged(); "
-                        + "Failed to notify IRadioCallbacks: " + e.getMessage());
-            }
-        }
-
-        @Override
-        public void onConfigurationChanged(RadioManager.BandConfig config) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "onConfigurationChanged(): config: " + config);
-            }
-
-            clearMetadata();
-
-            if (config != null) {
-                mCurrentRadioBand = config.getType();
-
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "onConfigurationChanged(): config type: " + mCurrentRadioBand);
-                }
-
-            }
-
-            try {
-                for (IRadioCallback callback : mRadioTunerCallbacks) {
-                    callback.onRadioBandChanged(mCurrentRadioBand);
-                }
-            } catch (RemoteException e) {
-                Log.e(TAG, "onConfigurationChanged(); "
-                        + "Failed to notify IRadioCallbacks: " + e.getMessage());
-            }
         }
 
         @Override
@@ -821,10 +431,7 @@
             // re-opening of the radio tuner.
             if (status == RadioTuner.ERROR_HARDWARE_FAILURE
                     || status == RadioTuner.ERROR_SERVER_DIED) {
-                if (mRadioTuner != null) {
-                    mRadioTuner.close();
-                    mRadioTuner = null;
-                }
+                close();
 
                 // Attempt to re-open the RadioTuner. Each time the radio tuner fails to open, the
                 // mReOpenRadioTunerCount will be incremented.
@@ -854,38 +461,41 @@
             }
 
             if (mRadioTuner == null) {
-                openRadioBandInternal(mCurrentRadioBand);
-            }
-        }
-
-        /**
-         * Sets all metadata fields to {@code null}.
-         */
-        private void clearMetadata() {
-            mCurrentChannelInfo = null;
-            mCurrentArtist = null;
-            mCurrentSongTitle = null;
-        }
-
-        /**
-         * Retrieves the relevant information off the given {@link RadioMetadata} object and
-         * sets them correspondingly on {@link #mCurrentChannelInfo}, {@link #mCurrentArtist}
-         * and {@link #mCurrentSongTitle}.
-         */
-        private void updateMetadata(RadioMetadata metadata) {
-            if (metadata != null) {
-                mCurrentChannelInfo = metadata.getString(RadioMetadata.METADATA_KEY_RDS_PS);
-                mCurrentArtist = metadata.getString(RadioMetadata.METADATA_KEY_ARTIST);
-                mCurrentSongTitle = metadata.getString(RadioMetadata.METADATA_KEY_TITLE);
-
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, String.format("updateMetadata(): [channel info: %s, artist: %s, "
-                            + "song title: %s]", mCurrentChannelInfo, mCurrentArtist,
-                            mCurrentSongTitle));
-                }
+                openRadioBandInternal(mRadioStorage.getStoredRadioBand());
             }
         }
     }
 
-    private final Runnable mOpenRadioTunerRunnable = () -> openRadioBandInternal(mCurrentRadioBand);
+    private final Runnable mOpenRadioTunerRunnable =
+            () -> openRadioBandInternal(mRadioStorage.getStoredRadioBand());
+
+    @Override
+    public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
+        /* Radio application may restrict who can read its MediaBrowser tree.
+         * Our implementation doesn't.
+         */
+        return mBrowseTree.getRoot();
+    }
+
+    @Override
+    public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) {
+        mBrowseTree.loadChildren(parentMediaId, result);
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        if (BrowseTree.ACTION_PLAY_BROADCASTRADIO.equals(intent.getAction())) {
+            Log.i(TAG, "Executing general play radio intent");
+            mMediaSession.getController().getTransportControls().playFromMediaId(
+                    mBrowseTree.getRoot().getRootId(), null);
+            return START_NOT_STICKY;
+        }
+
+        return super.onStartCommand(intent, flags, startId);
+    }
+
+    @Override
+    public IBinder asBinder() {
+        throw new UnsupportedOperationException("Not a binder");
+    }
 }
diff --git a/src/com/android/car/radio/RadioStorage.java b/src/com/android/car/radio/RadioStorage.java
deleted file mode 100644
index 3ce8297..0000000
--- a/src/com/android/car/radio/RadioStorage.java
+++ /dev/null
@@ -1,486 +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.car.radio;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.hardware.radio.RadioManager;
-import android.os.AsyncTask;
-import android.os.SystemProperties;
-import android.support.annotation.NonNull;
-import android.support.annotation.WorkerThread;
-import android.util.Log;
-import com.android.car.radio.service.RadioStation;
-import com.android.car.radio.demo.DemoRadioStations;
-import com.android.car.radio.demo.RadioDemo;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-/**
- * Class that manages persistent storage of various radio options.
- */
-public class RadioStorage {
-    private static final String TAG = "Em.RadioStorage";
-    private static final String PREF_NAME = "com.android.car.radio.RadioStorage";
-
-    // Keys used for storage in the SharedPreferences.
-    private static final String PREF_KEY_RADIO_BAND = "radio_band";
-    private static final String PREF_KEY_RADIO_CHANNEL_AM = "radio_channel_am";
-    private static final String PREF_KEY_RADIO_CHANNEL_FM = "radio_channel_fm";
-
-    public static final int INVALID_RADIO_CHANNEL = -1;
-    public static final int INVALID_RADIO_BAND = -1;
-
-    private static SharedPreferences sSharedPref;
-    private static RadioStorage sInstance;
-    private static RadioDatabase sRadioDatabase;
-
-    /**
-     * Listener that will be called when something in the radio storage changes.
-     */
-    public interface PresetsChangeListener {
-        /**
-         * Called when {@link #refreshPresets()} has completed.
-         */
-        void onPresetsRefreshed();
-    }
-
-    /**
-     * Listener that will be called when something in the pre-scanned channels has changed.
-     */
-    public interface PreScannedChannelChangeListener {
-        /**
-         * Notifies that the pre-scanned channels for the given radio band has changed.
-         *
-         * @param radioBand One of the band values in {@link RadioManager}.
-         */
-        void onPreScannedChannelChange(int radioBand);
-    }
-
-    private Set<PresetsChangeListener> mPresetListeners = new HashSet<>();
-
-    /**
-     * Set of listeners that will be notified whenever pre-scanned channels have changed.
-     *
-     * <p>Note that this set is not initialized because pre-scanned channels are only needed if
-     * dual-tuners exist in the current radio. Thus, this set is created conditionally.
-     */
-    private Set<PreScannedChannelChangeListener> mPreScannedListeners;
-
-    private List<RadioStation> mPresets = new ArrayList<>();
-
-    private RadioStorage(Context context) {
-        if (sSharedPref == null) {
-            sSharedPref = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
-        }
-
-        if (sRadioDatabase == null) {
-            sRadioDatabase = new RadioDatabase(context);
-        }
-    }
-
-    public static RadioStorage getInstance(Context context) {
-        if (sInstance == null) {
-            sInstance = new RadioStorage(context.getApplicationContext());
-
-            // When the RadioStorage is first created, load the list of radio presets.
-            sInstance.refreshPresets();
-        }
-
-        return sInstance;
-    }
-
-    /**
-     * Registers the given {@link PresetsChangeListener} to be notified when any radio preset state
-     * has changed.
-     */
-    public void addPresetsChangeListener(PresetsChangeListener listener) {
-        mPresetListeners.add(listener);
-    }
-
-    /**
-     * Unregisters the given {@link PresetsChangeListener}.
-     */
-    public void removePresetsChangeListener(PresetsChangeListener listener) {
-        mPresetListeners.remove(listener);
-    }
-
-    /**
-     * Registers the given {@link PreScannedChannelChangeListener} to be notified of changes to
-     * pre-scanned channels.
-     */
-    public void addPreScannedChannelChangeListener(PreScannedChannelChangeListener listener) {
-        if (mPreScannedListeners == null) {
-            mPreScannedListeners = new HashSet<>();
-        }
-
-        mPreScannedListeners.add(listener);
-    }
-
-    /**
-     * Unregisters the given {@link PreScannedChannelChangeListener}.
-     */
-    public void removePreScannedChannelChangeListener(PreScannedChannelChangeListener listener) {
-        if (mPreScannedListeners == null) {
-            return;
-        }
-
-        mPreScannedListeners.remove(listener);
-    }
-
-    /**
-     * Requests a load of all currently stored presets. This operation runs asynchronously. When
-     * the presets have been loaded, any registered {@link PresetsChangeListener}s are
-     * notified via the {@link PresetsChangeListener#onPresetsRefreshed()} method.
-     */
-    private void refreshPresets() {
-        new GetAllPresetsAsyncTask().execute();
-    }
-
-    /**
-     * Returns all currently loaded presets. If there are no stored presets, this method will
-     * return an empty {@link List}.
-     *
-     * <p>Register as a {@link PresetsChangeListener} to be notified of any changes in the
-     * preset list.
-     */
-    public List<RadioStation> getPresets() {
-        return mPresets;
-    }
-
-    /**
-     * Convenience method for checking if a specific channel is a preset. This method will assume
-     * the subchannel is 0.
-     *
-     * @see #isPreset(RadioStation)
-     * @return {@code true} if the channel is a user saved preset.
-     */
-    public boolean isPreset(int channel, int radioBand) {
-        return isPreset(new RadioStation(channel, 0 /* subchannel */, radioBand, null /* rds */));
-    }
-
-    /**
-     * Returns {@code true} if the given {@link RadioStation} is a user saved preset.
-     */
-    public boolean isPreset(RadioStation station) {
-        if (station == null) {
-            return false;
-        }
-
-        // Just iterate through the list and match the station. If we anticipate this list growing
-        // large, might have to change it to some sort of Set.
-        for (RadioStation preset : mPresets) {
-            if (preset.equals(station)) {
-                return true;
-            }
-        }
-
-        return false;
-    }
-
-    /**
-     * Stores that given {@link RadioStation} as a preset. This operation will override any
-     * previously stored preset that matches the given preset.
-     *
-     * <p>Upon a successful store, the presets list will be refreshed via a call to
-     * {@link #refreshPresets()}.
-     *
-     * @see #refreshPresets()
-     */
-    public void storePreset(RadioStation preset) {
-        if (preset == null) {
-            return;
-        }
-
-        new StorePresetAsyncTask().execute(preset);
-    }
-
-    /**
-     * Removes the given {@link RadioStation} as a preset.
-     *
-     * <p>Upon a successful removal, the presets list will be refreshed via a call to
-     * {@link #refreshPresets()}.
-     *
-     * @see #refreshPresets()
-     */
-    public void removePreset(RadioStation preset) {
-        if (preset == null) {
-            return;
-        }
-
-        new RemovePresetAsyncTask().execute(preset);
-    }
-
-    /**
-     * Returns the stored radio band that was set in {@link #storeRadioBand(int)}. If a radio band
-     * has not previously been stored, then {@link RadioManager#BAND_FM} is returned.
-     *
-     * @return One of {@link RadioManager#BAND_FM}, {@link RadioManager#BAND_AM},
-     * {@link RadioManager#BAND_FM_HD} or {@link RadioManager#BAND_AM_HD}.
-     */
-    public int getStoredRadioBand() {
-        // No need to verify that the returned value is one of AM_BAND or FM_BAND because this is
-        // done in storeRadioBand(int).
-        return sSharedPref.getInt(PREF_KEY_RADIO_BAND, RadioManager.BAND_FM);
-    }
-
-    /**
-     * Stores a radio band for later retrieval via {@link #getStoredRadioBand()}.
-     */
-    public void storeRadioBand(int radioBand) {
-        // Ensure that an incorrect radio band is not stored. Currently only FM and AM supported.
-        if (radioBand != RadioManager.BAND_FM && radioBand != RadioManager.BAND_AM) {
-            return;
-        }
-
-        sSharedPref.edit().putInt(PREF_KEY_RADIO_BAND, radioBand).apply();
-    }
-
-    /**
-     * Returns the stored radio channel that was set in {@link #storeRadioChannel(int, int)}. If a
-     * radio channel for the given band has not been previously stored, then
-     * {@link #INVALID_RADIO_CHANNEL} is returned.
-     *
-     * @param band One of the BAND_* values from {@link RadioManager}. For example,
-     *             {@link RadioManager#BAND_AM}.
-     */
-    public int getStoredRadioChannel(int band) {
-        switch (band) {
-            case RadioManager.BAND_AM:
-                return sSharedPref.getInt(PREF_KEY_RADIO_CHANNEL_AM, INVALID_RADIO_CHANNEL);
-
-            case RadioManager.BAND_FM:
-                return sSharedPref.getInt(PREF_KEY_RADIO_CHANNEL_FM, INVALID_RADIO_CHANNEL);
-
-            default:
-                return INVALID_RADIO_CHANNEL;
-        }
-    }
-
-    /**
-     * Stores a radio channel (i.e. the radio frequency) for a particular band so it can be later
-     * retrieved via {@link #getStoredRadioChannel(int band)}.
-     */
-    public void storeRadioChannel(int band, int channel) {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, String.format("storeRadioChannel(); band: %s, channel %s", band, channel));
-        }
-
-        if (channel <= 0) {
-            return;
-        }
-
-        switch (band) {
-            case RadioManager.BAND_AM:
-                sSharedPref.edit().putInt(PREF_KEY_RADIO_CHANNEL_AM, channel).apply();
-                break;
-
-            case RadioManager.BAND_FM:
-                sSharedPref.edit().putInt(PREF_KEY_RADIO_CHANNEL_FM, channel).apply();
-                break;
-
-            default:
-                Log.w(TAG, "Attempting to store channel for invalid band: " + band);
-        }
-    }
-
-    /**
-     * Stores the list of {@link RadioStation}s as the pre-scanned stations for the given radio
-     * band.
-     *
-     * @param radioBand One of {@link RadioManager#BAND_FM}, {@link RadioManager#BAND_AM},
-     * {@link RadioManager#BAND_FM_HD} or {@link RadioManager#BAND_AM_HD}.
-     */
-    public void storePreScannedStations(int radioBand, List<RadioStation> stations) {
-        if (stations == null) {
-            return;
-        }
-
-        // Converting to an array rather than passing a List to the execute to avoid any potential
-        // heap pollution via AsyncTask's varargs.
-        new StorePreScannedAsyncTask(radioBand).execute(
-                stations.toArray(new RadioStation[stations.size()]));
-    }
-
-    /**
-     * Returns the list of pre-scanned radio channels for the given band.
-     */
-    @NonNull
-    @WorkerThread
-    public List<RadioStation> getPreScannedStationsForBand(int radioBand) {
-        if (SystemProperties.getBoolean(RadioDemo.DEMO_MODE_PROPERTY, false)) {
-            switch (radioBand) {
-                case RadioManager.BAND_AM:
-                    return DemoRadioStations.getAmStations();
-
-                case RadioManager.BAND_FM:
-                default:
-                    return DemoRadioStations.getFmStations();
-
-            }
-        }
-
-        return sRadioDatabase.getAllPreScannedStationsForBand(radioBand);
-    }
-
-    /**
-     * Calls {@link PresetsChangeListener#onPresetsRefreshed()} for all registered
-     * {@link PresetsChangeListener}s.
-     */
-    private void notifyPresetsListeners() {
-        for (PresetsChangeListener listener : mPresetListeners) {
-            listener.onPresetsRefreshed();
-        }
-    }
-
-    /**
-     * Calls {@link PreScannedChannelChangeListener#onPreScannedChannelChange(int)} for all
-     * registered {@link PreScannedChannelChangeListener}s.
-     */
-    private void notifyPreScannedListeners(int radioBand) {
-        if (mPreScannedListeners == null) {
-            return;
-        }
-
-        for (PreScannedChannelChangeListener listener : mPreScannedListeners) {
-            listener.onPreScannedChannelChange(radioBand);
-        }
-    }
-
-    /**
-     * {@link AsyncTask} that will fetch all stored radio presets.
-     */
-    private class GetAllPresetsAsyncTask extends AsyncTask<Void, Void, Void> {
-        private static final String TAG = "Em.GetAllPresetsAT";
-
-        @Override
-        protected Void doInBackground(Void... voids) {
-            mPresets = sRadioDatabase.getAllPresets();
-
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "Loaded presets: " + mPresets);
-            }
-
-            return null;
-        }
-
-        @Override
-        public void onPostExecute(Void result) {
-            notifyPresetsListeners();
-        }
-    }
-
-    /**
-     * {@link AsyncTask} that will store a single {@link RadioStation} that is passed to its
-     * {@link AsyncTask#execute(Object[])}.
-     */
-    private class StorePresetAsyncTask extends AsyncTask<RadioStation, Void, Boolean> {
-        private static final String TAG = "Em.StorePresetAT";
-
-        @Override
-        protected Boolean doInBackground(RadioStation... radioStations) {
-            RadioStation presetToStore = radioStations[0];
-            boolean result = sRadioDatabase.insertPreset(presetToStore);
-
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "Store preset success: " + result);
-            }
-
-            if (result) {
-                // Refresh the presets list.
-                mPresets = sRadioDatabase.getAllPresets();
-            }
-
-            return result;
-        }
-
-        @Override
-        public void onPostExecute(Boolean result) {
-            if (result) {
-                notifyPresetsListeners();
-            }
-        }
-    }
-
-    /**
-     * {@link AsyncTask} that will remove a single {@link RadioStation} that is passed to its
-     * {@link AsyncTask#execute(Object[])}.
-     */
-    private class RemovePresetAsyncTask extends AsyncTask<RadioStation, Void, Boolean> {
-        private static final String TAG = "Em.RemovePresetAT";
-
-        @Override
-        protected Boolean doInBackground(RadioStation... radioStations) {
-            RadioStation presetToStore = radioStations[0];
-            boolean result = sRadioDatabase.deletePreset(presetToStore);
-
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "Remove preset success: " + result);
-            }
-
-            if (result) {
-                // Refresh the presets list.
-                mPresets = sRadioDatabase.getAllPresets();
-            }
-
-            return result;
-        }
-
-        @Override
-        public void onPostExecute(Boolean result) {
-            if (result) {
-                notifyPresetsListeners();
-            }
-        }
-    }
-
-    /**
-     * {@link AsyncTask} that will store a list of pre-scanned {@link RadioStation}s that is passed
-     * to its {@link AsyncTask#execute(Object[])}.
-     */
-    private class StorePreScannedAsyncTask extends AsyncTask<RadioStation, Void, Boolean> {
-        private static final String TAG = "Em.StorePreScannedAT";
-        private final int mRadioBand;
-
-        public StorePreScannedAsyncTask(int radioBand) {
-            mRadioBand = radioBand;
-        }
-
-        @Override
-        protected Boolean doInBackground(RadioStation... radioStations) {
-            boolean result = sRadioDatabase.insertPreScannedStations(mRadioBand,
-                    Arrays.asList(radioStations));
-
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "Store pre-scanned stations success: " + result);
-            }
-
-            return result;
-        }
-
-        @Override
-        public void onPostExecute(Boolean result) {
-            if (result) {
-                notifyPreScannedListeners(mRadioBand);
-            }
-        }
-    }
-}
diff --git a/src/com/android/car/radio/audio/AudioStreamController.java b/src/com/android/car/radio/audio/AudioStreamController.java
new file mode 100644
index 0000000..071f83a
--- /dev/null
+++ b/src/com/android/car/radio/audio/AudioStreamController.java
@@ -0,0 +1,241 @@
+/**
+ * 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.car.radio.audio;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioFocusRequest;
+import android.media.AudioManager;
+import android.os.RemoteException;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
+
+import com.android.car.radio.platform.RadioManagerExt;
+import com.android.car.radio.platform.RadioTunerExt;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Manages radio's audio stream.
+ */
+public class AudioStreamController {
+    private static final String TAG = "BcRadioApp.AudioSCntrl";
+
+    private final Object mLock = new Object();
+    private final AudioManager mAudioManager;
+    private final RadioTunerExt mRadioTunerExt;
+
+    private final AudioFocusRequest mGainFocusReq;
+
+    /**
+     * Indicates that the app has *some* focus or a promise of it.
+     *
+     * It may be ducked, transiently lost or delayed.
+     */
+    private boolean mHasSomeFocus = false;
+
+    private boolean mIsTuning = false;
+
+    private int mCurrentPlaybackState = PlaybackStateCompat.STATE_NONE;
+    private final List<IPlaybackStateListener> mPlaybackStateListeners = new ArrayList<>();
+
+    public AudioStreamController(@NonNull Context context, @NonNull RadioManagerExt radioManager) {
+        mAudioManager = Objects.requireNonNull(
+                (AudioManager) context.getSystemService(Context.AUDIO_SERVICE));
+        mRadioTunerExt = Objects.requireNonNull(radioManager.getRadioTunerExt());
+
+        AudioAttributes playbackAttr = new AudioAttributes.Builder()
+                .setUsage(AudioAttributes.USAGE_MEDIA)
+                .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+                .build();
+        mGainFocusReq = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
+                .setAudioAttributes(playbackAttr)
+                .setAcceptsDelayedFocusGain(true)
+                .setWillPauseWhenDucked(true)
+                .setOnAudioFocusChangeListener(this::onAudioFocusChange)
+                .build();
+    }
+
+    /**
+     * Add playback state listener.
+     *
+     * @param listener listener to add
+     */
+    public void addPlaybackStateListener(@NonNull IPlaybackStateListener listener) {
+        synchronized (mLock) {
+            mPlaybackStateListeners.add(Objects.requireNonNull(listener));
+            try {
+                listener.onPlaybackStateChanged(mCurrentPlaybackState);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Couldn't notify new listener about current playback state", e);
+            }
+        }
+    }
+
+    /**
+     * Remove playback state listener.
+     *
+     * @param listener listener to remove
+     */
+    public void removePlaybackStateListener(IPlaybackStateListener listener) {
+        synchronized (mLock) {
+            mPlaybackStateListeners.remove(listener);
+        }
+    }
+
+    private void notifyPlaybackStateChangedLocked(@PlaybackStateCompat.State int state) {
+        if (mCurrentPlaybackState == state) return;
+        mCurrentPlaybackState = state;
+        for (IPlaybackStateListener listener : mPlaybackStateListeners) {
+            try {
+                listener.onPlaybackStateChanged(state);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Couldn't notify listener about playback state change", e);
+            }
+        }
+    }
+
+    private boolean requestAudioFocusLocked() {
+        if (mHasSomeFocus) return true;
+        int res = mAudioManager.requestAudioFocus(mGainFocusReq);
+        if (res == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
+            Log.i(TAG, "Audio focus request is delayed");
+            mHasSomeFocus = true;
+            return true;
+        }
+        if (res != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+            Log.w(TAG, "Couldn't obtain audio focus, res=" + res);
+            return false;
+        }
+
+        Log.v(TAG, "Audio focus request succeeded");
+        mHasSomeFocus = true;
+
+        // we assume that audio focus was requested only when we mean to unmute
+        if (!mRadioTunerExt.setMuted(false)) return false;
+
+        return true;
+    }
+
+    private boolean abandonAudioFocusLocked() {
+        if (!mHasSomeFocus) return true;
+        if (!mRadioTunerExt.setMuted(true)) return false;
+
+        int res = mAudioManager.abandonAudioFocusRequest(mGainFocusReq);
+        if (res != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+            Log.e(TAG, "Couldn't abandon audio focus, res=" + res);
+            return false;
+        }
+
+        Log.v(TAG, "Audio focus abandoned");
+        mHasSomeFocus = false;
+
+        return true;
+    }
+
+    /**
+     * Prepare playback for ongoing tune/scan operation.
+     *
+     * @param skipDirectionNext true if it's skipping to next station;
+     *                          false if skipping to previous;
+     *                          empty if tuning to arbitrary selector.
+     */
+    public boolean preparePlayback(Optional<Boolean> skipDirectionNext) {
+        synchronized (mLock) {
+            if (!requestAudioFocusLocked()) return false;
+
+            int state = PlaybackStateCompat.STATE_CONNECTING;
+            if (skipDirectionNext.isPresent()) {
+                state = skipDirectionNext.get() ? PlaybackStateCompat.STATE_SKIPPING_TO_NEXT
+                        : PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS;
+            }
+            notifyPlaybackStateChangedLocked(state);
+
+            mIsTuning = true;
+            return true;
+        }
+    }
+
+    /**
+     * Notifies AudioStreamController that radio hardware is done with tune/scan operation.
+     *
+     * TODO(b/73950974): use callbacks, don't hardcode
+     *
+     * @see #preparePlayback
+     */
+    public void notifyProgramInfoChanged() {
+        synchronized (mLock) {
+            if (!mIsTuning) return;
+            mIsTuning = false;
+            notifyPlaybackStateChangedLocked(PlaybackStateCompat.STATE_PLAYING);
+        }
+    }
+
+    /**
+     * Request audio stream muted or unmuted.
+     *
+     * @param muted true, if audio stream should be muted, false if unmuted
+     * @return true, if request has succeeded (maybe delayed)
+     */
+    public boolean requestMuted(boolean muted) {
+        synchronized (mLock) {
+            if (muted) {
+                notifyPlaybackStateChangedLocked(PlaybackStateCompat.STATE_STOPPED);
+                return abandonAudioFocusLocked();
+            } else {
+                if (!requestAudioFocusLocked()) return false;
+                notifyPlaybackStateChangedLocked(PlaybackStateCompat.STATE_PLAYING);
+                return true;
+            }
+        }
+    }
+
+    // TODO(b/73950974): depend on callbacks only
+    public boolean isMuted() {
+        return !mHasSomeFocus;
+    }
+
+    private void onAudioFocusChange(int focusChange) {
+        Log.v(TAG, "onAudioFocusChange(" + focusChange + ")");
+
+        synchronized (mLock) {
+            switch (focusChange) {
+                case AudioManager.AUDIOFOCUS_GAIN:
+                    mHasSomeFocus = true;
+                    // we assume that audio focus was requested only when we mean to unmute
+                    mRadioTunerExt.setMuted(false);
+                    break;
+                case AudioManager.AUDIOFOCUS_LOSS:
+                    Log.i(TAG, "Unexpected audio focus loss");
+                    mHasSomeFocus = false;
+                    mRadioTunerExt.setMuted(true);
+                    notifyPlaybackStateChangedLocked(PlaybackStateCompat.STATE_STOPPED);
+                    break;
+                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+                    mRadioTunerExt.setMuted(true);
+                    break;
+                default:
+                    Log.w(TAG, "Unexpected audio focus state: " + focusChange);
+            }
+        }
+    }
+}
diff --git a/src/com/android/car/radio/audio/IPlaybackStateListener.aidl b/src/com/android/car/radio/audio/IPlaybackStateListener.aidl
new file mode 100644
index 0000000..367ec27
--- /dev/null
+++ b/src/com/android/car/radio/audio/IPlaybackStateListener.aidl
@@ -0,0 +1,21 @@
+/**
+ * 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.car.radio.audio;
+
+oneway interface IPlaybackStateListener {
+    void onPlaybackStateChanged(int state);
+}
diff --git a/src/com/android/car/radio/demo/DemoRadioStations.java b/src/com/android/car/radio/demo/DemoRadioStations.java
deleted file mode 100644
index 3bc0e5d..0000000
--- a/src/com/android/car/radio/demo/DemoRadioStations.java
+++ /dev/null
@@ -1,73 +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.car.radio.demo;
-
-import android.hardware.radio.RadioManager;
-import com.android.car.radio.service.RadioRds;
-import com.android.car.radio.service.RadioStation;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * Static lists of the mock radio stations for AM and FM bands.
- */
-public class DemoRadioStations {
-    private static final List<RadioStation> mFmStations = Arrays.asList(
-        new RadioStation(94900, 0, RadioManager.BAND_FM,
-                new RadioRds("Wild 94.9", "Drake ft. Rihanna", "Too Good")),
-        new RadioStation(96500, 0, RadioManager.BAND_FM,
-                new RadioRds("KOIT", "Celine Dion", "All By Myself")),
-        new RadioStation(97300, 0, RadioManager.BAND_FM,
-                new RadioRds("Alice@97.3", "Drops of Jupiter", "Train")),
-        new RadioStation(99700, 0, RadioManager.BAND_FM,
-                new RadioRds("99.7 Now!", "The Chainsmokers", "Closer")),
-        new RadioStation(101300, 0, RadioManager.BAND_FM,
-                new RadioRds("101-3 KISS-FM", "Justin Timberlake", "Rock Your Body")),
-        new RadioStation(103700, 0, RadioManager.BAND_FM,
-                new RadioRds("iHeart80s @ 103.7", "Michael Jackson", "Billie Jean")),
-        new RadioStation(106100, 0, RadioManager.BAND_FM,
-                new RadioRds("106 KMEL", "Drake", "Marvins Room")));
-
-    private static final List<RadioStation> mAmStations = Arrays.asList(
-        new RadioStation(530, 0, RadioManager.BAND_AM, null),
-        new RadioStation(610, 0, RadioManager.BAND_AM, null),
-        new RadioStation(730, 0, RadioManager.BAND_AM, null),
-        new RadioStation(801, 0, RadioManager.BAND_AM, null),
-        new RadioStation(930, 0, RadioManager.BAND_AM, null),
-        new RadioStation(1100, 0, RadioManager.BAND_AM, null),
-        new RadioStation(1480, 0, RadioManager.BAND_AM, null),
-        new RadioStation(1530, 0, RadioManager.BAND_AM, null));
-
-    /**
-     * Returns a list of {@link RadioStation}s that represent all the FM channels.
-     */
-    public static List<RadioStation> getFmStations() {
-        // Create a new list so that the user of the list can modify the contents, but the main
-        // list will remain unchanged in this class.
-        return new ArrayList<>(mFmStations);
-    }
-
-    /**
-     * Returns a list of {@link RadioStation}s that represent all the AM channels.
-     */
-    public static List<RadioStation> getAmStations() {
-        // Create a new list so that the user of the list can modify the contents, but the main
-        // list will remain unchanged in this class.
-        return new ArrayList<>(mAmStations);
-    }
-}
diff --git a/src/com/android/car/radio/demo/RadioDemo.java b/src/com/android/car/radio/demo/RadioDemo.java
deleted file mode 100644
index 1c5a339..0000000
--- a/src/com/android/car/radio/demo/RadioDemo.java
+++ /dev/null
@@ -1,394 +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.car.radio.demo;
-
-import android.car.hardware.radio.CarRadioManager;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.hardware.radio.RadioManager;
-import android.media.AudioAttributes;
-import android.media.AudioManager;
-import android.os.RemoteException;
-import android.os.SystemProperties;
-import android.support.car.Car;
-import android.support.car.CarNotConnectedException;
-import android.support.car.CarConnectionCallback;
-import android.support.car.media.CarAudioManager;
-import android.util.Log;
-import com.android.car.radio.service.IRadioCallback;
-import com.android.car.radio.service.IRadioManager;
-import com.android.car.radio.service.RadioStation;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A demo {@link IRadiomanager} that has a fixed set of AM and FM stations.
- */
-public class RadioDemo implements AudioManager.OnAudioFocusChangeListener {
-    private static final String TAG = "RadioDemo";
-
-    /**
-     * The property name to enable demo mode.
-     */
-    public static final String DEMO_MODE_PROPERTY = "com.android.car.radio.demo";
-
-    /**
-     * The property name to enable the radio in demo mode with dual tuners.
-     */
-    public static final String DUAL_DEMO_MODE_PROPERTY = "com.android.car.radio.demo.dual";
-
-    private static RadioDemo sInstance;
-    private List<IRadioCallback> mCallbacks = new ArrayList<>();
-
-    private List<RadioStation> mCurrentStations = new ArrayList<>();
-    private int mCurrentRadioBand = RadioManager.BAND_FM;
-
-    private Car mCarApi;
-    private CarAudioManager mCarAudioManager;
-    private AudioAttributes mRadioAudioAttributes;
-
-    private boolean mHasAudioFocus;
-
-    private int mCurrentIndex;
-    private boolean mIsMuted;
-
-    private RadioDemo(Context context) {
-        if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
-            mCarApi = Car.createCar(context, mCarConnectionCallback);
-            mCarApi.connect();
-        }
-    }
-
-    /**
-     * Returns a mock {@link IRadioManager} to use for demo purposes. The returned class will have
-     * a fixed list of AM and FM changegs and support all the IRadioManager's functionality.
-     */
-    public IRadioManager.Stub createDemoManager() {
-        return new IRadioManager.Stub() {
-            @Override
-            public void tune(RadioStation station) throws RemoteException {
-                if (station == null || !requestAudioFocus()) {
-                    return;
-                }
-
-                if (station.getRadioBand() != mCurrentRadioBand) {
-                    switchRadioBand(station.getRadioBand());
-                }
-
-                boolean found = false;
-
-                for (int i = 0, size = mCurrentStations.size(); i < size; i++) {
-                    RadioStation storedStation = mCurrentStations.get(i);
-
-                    if (storedStation.equals(station)) {
-                        found = true;
-                        mCurrentIndex = i;
-                        break;
-                    }
-                }
-
-                // If not found, then insert it into the list, sorted by the channel.
-                if (!found) {
-                    int indexToInsert = 0;
-
-                    for (int i = 0, size = mCurrentStations.size(); i < size; i++) {
-                        RadioStation storedStation = mCurrentStations.get(i);
-
-                        if (station.getChannelNumber() >= storedStation.getChannelNumber()) {
-                            indexToInsert = i + 1;
-                            break;
-                        }
-                    }
-
-                    RadioStation stationToInsert = new RadioStation(station.getChannelNumber(),
-                            0 /* subChannel */, station.getRadioBand(), null /* rds */);
-                    mCurrentStations.add(indexToInsert, stationToInsert);
-
-                    mCurrentIndex = indexToInsert;
-                }
-
-                notifyCallbacks(station);
-            }
-
-            @Override
-            public void seekForward() throws RemoteException {
-                if (!requestAudioFocus()) {
-                    return;
-                }
-
-                if (++mCurrentIndex >= mCurrentStations.size()) {
-                    mCurrentIndex = 0;
-                }
-
-                notifyCallbacks(mCurrentStations.get(mCurrentIndex));
-            }
-
-            @Override
-            public void seekBackward() throws RemoteException {
-                if (!requestAudioFocus()) {
-                    return;
-                }
-
-                if (--mCurrentIndex < 0){
-                    mCurrentIndex = mCurrentStations.size() - 1;
-                }
-
-                notifyCallbacks(mCurrentStations.get(mCurrentIndex));
-            }
-
-            @Override
-            public boolean mute() throws RemoteException {
-                mIsMuted = true;
-                notifyCallbacksMuteChanged(mIsMuted);
-                return mIsMuted;
-            }
-
-            @Override
-            public boolean unMute() throws RemoteException {
-                requestAudioFocus();
-
-                if (mHasAudioFocus) {
-                    mIsMuted = false;
-                }
-
-                notifyCallbacksMuteChanged(mIsMuted);
-                return !mIsMuted;
-            }
-
-            @Override
-            public boolean isMuted() throws RemoteException {
-                return mIsMuted;
-            }
-
-            @Override
-            public int openRadioBand(int radioBand) throws RemoteException {
-                if (!requestAudioFocus()) {
-                    return RadioManager.STATUS_ERROR;
-                }
-
-                switchRadioBand(radioBand);
-                notifyCallbacks(radioBand);
-                return RadioManager.STATUS_OK;
-            }
-
-            @Override
-            public void addRadioTunerCallback(IRadioCallback callback) throws RemoteException {
-                mCallbacks.add(callback);
-            }
-
-            @Override
-            public void removeRadioTunerCallback(IRadioCallback callback) throws RemoteException {
-                mCallbacks.remove(callback);
-            }
-
-            @Override
-            public RadioStation getCurrentRadioStation() throws RemoteException {
-                return mCurrentStations.get(mCurrentIndex);
-            }
-
-            @Override
-            public boolean isInitialized() throws RemoteException {
-                return true;
-            }
-
-            @Override
-            public boolean hasFocus() {
-                return mHasAudioFocus;
-            }
-
-            @Override
-            public boolean hasDualTuners() throws RemoteException {
-                return SystemProperties.getBoolean(RadioDemo.DUAL_DEMO_MODE_PROPERTY, false);
-            }
-        };
-    }
-
-    @Override
-    public void onAudioFocusChange(int focusChange) {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "focus change: " + focusChange);
-        }
-
-        switch (focusChange) {
-            case AudioManager.AUDIOFOCUS_GAIN:
-                mHasAudioFocus = true;
-                break;
-
-            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
-            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
-                mHasAudioFocus = false;
-                break;
-
-            case AudioManager.AUDIOFOCUS_LOSS:
-                abandonAudioFocus();
-                break;
-
-            default:
-                // Do nothing for all other cases.
-        }
-    }
-
-    /**
-     * Requests audio focus for the current application.
-     *
-     * @return {@code true} if the request succeeded.
-     */
-    private boolean requestAudioFocus() {
-        if (mCarAudioManager == null) {
-            return false;
-        }
-
-        int status = AudioManager.AUDIOFOCUS_REQUEST_FAILED;
-        try {
-            status = mCarAudioManager.requestAudioFocus(this, mRadioAudioAttributes,
-                    AudioManager.AUDIOFOCUS_GAIN, 0);
-        } catch (CarNotConnectedException e) {
-            Log.e(TAG, "requestAudioFocus() failed", e);
-        }
-
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "requestAudioFocus status: " + status);
-        }
-
-        if (status == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
-            mHasAudioFocus = true;
-        }
-
-        return status == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
-    }
-
-    /**
-     * Abandons audio focus for the current application.
-     *
-     * @return {@code true} if the request succeeded.
-     */
-    private void abandonAudioFocus() {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "abandonAudioFocus()");
-        }
-
-        if (mCarAudioManager == null) {
-            return;
-        }
-
-        mCarAudioManager.abandonAudioFocus(this, mRadioAudioAttributes);
-    }
-
-    /**
-     * {@link CarConnectionCallback} that retrieves the {@link CarRadioManager}.
-     */
-    private final CarConnectionCallback mCarConnectionCallback =
-            new CarConnectionCallback() {
-                @Override
-                public void onConnected(Car car) {
-                    if (Log.isLoggable(TAG, Log.DEBUG)) {
-                        Log.d(TAG, "Car service connected.");
-                    }
-                    try {
-                        // The CarAudioManager only needs to be retrieved once.
-                        if (mCarAudioManager == null) {
-                            mCarAudioManager = (CarAudioManager) mCarApi.getCarManager(
-                                    android.car.Car.AUDIO_SERVICE);
-
-                            mRadioAudioAttributes = mCarAudioManager.getAudioAttributesForCarUsage(
-                                    CarAudioManager.CAR_AUDIO_USAGE_RADIO);
-                        }
-                    } catch (CarNotConnectedException e) {
-                        //TODO finish
-                        Log.e(TAG, "Car not connected");
-                    }
-                }
-
-                @Override
-                public void onDisconnected(Car car) {
-                    if (Log.isLoggable(TAG, Log.DEBUG)) {
-                        Log.d(TAG, "Car service disconnected.");
-                    }
-                }
-            };
-
-    /**
-     * Switches to the corresponding radio band. This will update the list of current stations
-     * as well as notify any callbacks.
-     */
-    private void switchRadioBand(int radioBand) {
-        switch (radioBand) {
-            case RadioManager.BAND_AM:
-                mCurrentStations = DemoRadioStations.getAmStations();
-                break;
-            case RadioManager.BAND_FM:
-                mCurrentStations = DemoRadioStations.getFmStations();
-                break;
-            default:
-                mCurrentStations = new ArrayList<>();
-        }
-
-        mCurrentRadioBand = radioBand;
-        mCurrentIndex = 0;
-
-        notifyCallbacks(mCurrentRadioBand);
-        notifyCallbacks(mCurrentStations.get(mCurrentIndex));
-    }
-
-    /**
-     * Notifies any {@link IRadioCallback} that the mute state of the radio has changed.
-     */
-    private void notifyCallbacksMuteChanged(boolean isMuted) {
-        for (IRadioCallback callback : mCallbacks) {
-            try {
-                callback.onRadioMuteChanged(isMuted);
-            } catch (RemoteException e) {
-                // Ignore.
-            }
-        }
-    }
-
-    /**
-     * Notifies any {@link IRadioCallback}s that the radio band has changed.
-     */
-    private void notifyCallbacks(int radioBand) {
-        for (IRadioCallback callback : mCallbacks) {
-            try {
-                callback.onRadioBandChanged(radioBand);
-            } catch (RemoteException e) {
-                // Ignore.
-            }
-        }
-    }
-
-    /**
-     * Notifies any {@link IRadioCallback}s that the radio station has been changed to the given
-     * {@link RadioStation}.
-     */
-    private void notifyCallbacks(RadioStation station) {
-        for (IRadioCallback callback : mCallbacks) {
-            try {
-                callback.onRadioStationChanged(station);
-            } catch (RemoteException e) {
-                // Ignore.
-            }
-        }
-    }
-
-    public static RadioDemo getInstance(Context context) {
-        if (sInstance == null) {
-            sInstance = new RadioDemo(context);
-        }
-
-        return sInstance;
-    }
-}
diff --git a/src/com/android/car/radio/media/TunerSession.java b/src/com/android/car/radio/media/TunerSession.java
new file mode 100644
index 0000000..3b77057
--- /dev/null
+++ b/src/com/android/car/radio/media/TunerSession.java
@@ -0,0 +1,199 @@
+/**
+ * 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.car.radio.media;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager.ProgramInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
+
+import com.android.car.broadcastradio.support.Program;
+import com.android.car.broadcastradio.support.media.BrowseTree;
+import com.android.car.broadcastradio.support.platform.ImageResolver;
+import com.android.car.broadcastradio.support.platform.ProgramInfoExt;
+import com.android.car.broadcastradio.support.platform.ProgramSelectorExt;
+import com.android.car.radio.R;
+import com.android.car.radio.audio.IPlaybackStateListener;
+import com.android.car.radio.service.IRadioManager;
+import com.android.car.radio.utils.ThrowingRunnable;
+
+import java.util.Objects;
+
+/**
+ * Implementation of tuner's MediaSession.
+ */
+public class TunerSession extends MediaSessionCompat implements IPlaybackStateListener {
+    private static final String TAG = "BcRadioApp.msess";
+
+    private final Object mLock = new Object();
+
+    private final Context mContext;
+    private final BrowseTree mBrowseTree;
+    @Nullable private final ImageResolver mImageResolver;
+    private final IRadioManager mUiSession;
+    private final PlaybackStateCompat.Builder mPlaybackStateBuilder =
+            new PlaybackStateCompat.Builder();
+    @Nullable private ProgramInfo mCurrentProgram;
+
+    public TunerSession(@NonNull Context context, @NonNull BrowseTree browseTree,
+            @NonNull IRadioManager uiSession, @Nullable ImageResolver imageResolver) {
+        super(context, TAG);
+
+        mContext = Objects.requireNonNull(context);
+        mBrowseTree = Objects.requireNonNull(browseTree);
+        mImageResolver = imageResolver;
+        mUiSession = Objects.requireNonNull(uiSession);
+
+        // ACTION_PAUSE is reserved for time-shifted playback
+        mPlaybackStateBuilder.setActions(
+                PlaybackStateCompat.ACTION_STOP
+                | PlaybackStateCompat.ACTION_PLAY
+                | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
+                | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
+                | PlaybackStateCompat.ACTION_SET_RATING
+                | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
+                | PlaybackStateCompat.ACTION_PLAY_FROM_URI);
+        setRatingType(RatingCompat.RATING_HEART);
+        onPlaybackStateChanged(PlaybackStateCompat.STATE_NONE);
+        setCallback(new TunerSessionCallback());
+
+        setActive(true);
+    }
+
+    private void updateMetadata() {
+        synchronized (mLock) {
+            if (mCurrentProgram == null) return;
+            boolean fav = mBrowseTree.isFavorite(mCurrentProgram.getSelector());
+            setMetadata(MediaMetadataCompat.fromMediaMetadata(
+                    ProgramInfoExt.toMediaMetadata(mCurrentProgram, fav, mImageResolver)));
+        }
+    }
+
+    public void notifyProgramInfoChanged(@NonNull ProgramInfo info) {
+        synchronized (mLock) {
+            mCurrentProgram = info;
+            updateMetadata();
+        }
+    }
+
+    @Override
+    public void onPlaybackStateChanged(@PlaybackStateCompat.State int state) {
+        synchronized (mPlaybackStateBuilder) {
+            mPlaybackStateBuilder.setState(state,
+                    PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f);
+            setPlaybackState(mPlaybackStateBuilder.build());
+        }
+    }
+
+    public void notifyFavoritesChanged() {
+        updateMetadata();
+    }
+
+    private void selectionError() {
+        exec(() -> mUiSession.mute());
+        mPlaybackStateBuilder.setErrorMessage(mContext.getString(R.string.invalid_selection));
+        onPlaybackStateChanged(PlaybackStateCompat.STATE_ERROR);
+        mPlaybackStateBuilder.setErrorMessage(null);
+    }
+
+    private void exec(ThrowingRunnable<RemoteException> func) {
+        try {
+            func.run();
+        } catch (RemoteException ex) {
+            Log.e(TAG, "Failed to execute MediaSession callback", ex);
+        }
+    }
+
+    private class TunerSessionCallback extends MediaSessionCompat.Callback {
+        @Override
+        public void onStop() {
+            exec(() -> mUiSession.mute());
+        }
+
+        @Override
+        public void onPlay() {
+            exec(() -> mUiSession.unMute());
+        }
+
+        @Override
+        public void onSkipToNext() {
+            exec(() -> mUiSession.seekForward());
+        }
+
+        @Override
+        public void onSkipToPrevious() {
+            exec(() -> mUiSession.seekBackward());
+        }
+
+        @Override
+        public void onSetRating(RatingCompat rating) {
+            synchronized (mLock) {
+                if (mCurrentProgram == null) return;
+                if (rating.hasHeart()) {
+                    Program fav = Program.fromProgramInfo(mCurrentProgram);
+                    exec(() -> mUiSession.addFavorite(fav));
+                } else {
+                    ProgramSelector fav = mCurrentProgram.getSelector();
+                    exec(() -> mUiSession.removeFavorite(fav));
+                }
+            }
+        }
+
+        @Override
+        public void onPlayFromMediaId(String mediaId, Bundle extras) {
+            if (mBrowseTree.getRoot().getRootId().equals(mediaId)) {
+                // general play command
+                onPlay();
+                return;
+            }
+
+            ProgramSelector selector = mBrowseTree.parseMediaId(mediaId);
+            if (selector != null) {
+                exec(() -> mUiSession.tune(selector));
+            } else {
+                Log.w(TAG, "Invalid media ID: " + mediaId);
+                selectionError();
+            }
+        }
+
+        @Override
+        public void onPlayFromUri(Uri uri, Bundle extras) {
+            ProgramSelector selector = ProgramSelectorExt.fromUri(uri);
+            if (selector != null) {
+                exec(() -> mUiSession.tune(selector));
+            } else {
+                Log.w(TAG, "Invalid URI: " + uri);
+                selectionError();
+            }
+        }
+    }
+
+    @Override
+    public IBinder asBinder() {
+        throw new UnsupportedOperationException("Not a binder");
+    }
+}
diff --git a/src/com/android/car/radio/platform/ImageMemoryCache.java b/src/com/android/car/radio/platform/ImageMemoryCache.java
new file mode 100644
index 0000000..1cd1ba2
--- /dev/null
+++ b/src/com/android/car/radio/platform/ImageMemoryCache.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.car.radio.platform;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Bitmap;
+
+import com.android.car.broadcastradio.support.platform.ImageResolver;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+
+public class ImageMemoryCache implements ImageResolver {
+    private final RadioManagerExt mRadioManager;
+    private final Map<Long, Bitmap> mCache;
+
+    public ImageMemoryCache(@NonNull RadioManagerExt radioManager, int cacheSize) {
+        mRadioManager = Objects.requireNonNull(radioManager);
+        mCache = new CacheMap<>(cacheSize);
+    }
+
+    public @Nullable Bitmap resolve(long globalId) {
+        synchronized (mCache) {
+            if (mCache.containsKey(globalId)) return mCache.get(globalId);
+
+            Bitmap bm = mRadioManager.getMetadataImage(globalId);
+            mCache.put(globalId, bm);
+            return bm;
+        }
+    }
+
+    private static class CacheMap<K, V> extends LinkedHashMap<K, V> {
+        private final int mMaxSize;
+
+        public CacheMap(int maxSize) {
+            if (maxSize < 0) throw new IllegalArgumentException("maxSize must not be negative");
+            mMaxSize = maxSize;
+        }
+
+        @Override
+        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
+            return size() > mMaxSize;
+        }
+    }
+}
diff --git a/src/com/android/car/radio/platform/RadioManagerExt.java b/src/com/android/car/radio/platform/RadioManagerExt.java
new file mode 100644
index 0000000..46e7a7d
--- /dev/null
+++ b/src/com/android/car/radio/platform/RadioManagerExt.java
@@ -0,0 +1,159 @@
+/**
+ * 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.car.radio.platform;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.hardware.radio.RadioManager;
+import android.hardware.radio.RadioManager.BandDescriptor;
+import android.hardware.radio.RadioTuner;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Log;
+
+import com.android.car.broadcastradio.support.platform.RadioMetadataExt;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * Proposed extensions to android.hardware.radio.RadioManager.
+ *
+ * They might eventually get pushed to the framework.
+ */
+public class RadioManagerExt {
+    private static final String TAG = "BcRadioApp.mgrext";
+
+    // For now, we open first radio module only.
+    private static final int HARDCODED_MODULE_INDEX = 0;
+
+    private final Object mLock = new Object();
+
+    private final HandlerThread mCallbackHandlerThread = new HandlerThread("BcRadioApp.cbhandler");
+
+    private final @NonNull RadioManager mRadioManager;
+    private final RadioTunerExt mRadioTunerExt;
+    private List<RadioManager.ModuleProperties> mModules;
+    private @Nullable List<BandDescriptor> mAmFmRegionConfig;
+
+    public RadioManagerExt(@NonNull Context ctx) {
+        mRadioManager = (RadioManager)ctx.getSystemService(Context.RADIO_SERVICE);
+        Objects.requireNonNull(mRadioManager, "RadioManager could not be loaded");
+        mRadioTunerExt = new RadioTunerExt(ctx);
+        mCallbackHandlerThread.start();
+    }
+
+    public RadioTunerExt getRadioTunerExt() {
+        return mRadioTunerExt;
+    }
+
+    /* Select only one region. HAL 2.x moves region selection responsibility from the app to the
+     * Broadcast Radio service, so we won't implement region selection based on bands in the app.
+     */
+    private @Nullable List<BandDescriptor> reduceAmFmBands(@Nullable BandDescriptor[] bands) {
+        if (bands == null || bands.length == 0) return null;
+        int region = bands[0].getRegion();
+        Log.d(TAG, "Auto-selecting region " + region);
+
+        return Arrays.stream(bands).filter(band -> band.getRegion() == region).
+                collect(Collectors.toList());
+    }
+
+    private void initModules() {
+        synchronized (mLock) {
+            if (mModules != null) return;
+
+            mModules = new ArrayList<>();
+            int status = mRadioManager.listModules(mModules);
+            if (status != RadioManager.STATUS_OK) {
+                Log.w(TAG, "Couldn't get radio module list: " + status);
+                return;
+            }
+
+            if (mModules.size() == 0) {
+                Log.i(TAG, "No radio modules on this device");
+                return;
+            }
+
+            RadioManager.ModuleProperties module = mModules.get(HARDCODED_MODULE_INDEX);
+            mAmFmRegionConfig = reduceAmFmBands(module.getBands());
+        }
+    }
+
+    public @Nullable RadioTuner openSession(RadioTuner.Callback callback, Handler handler) {
+        Log.i(TAG, "Opening broadcast radio session...");
+
+        initModules();
+        if (mModules.size() == 0) return null;
+
+        /* We won't need custom default wrapper when we push these proposed extensions to the
+         * framework; this is solely to avoid deadlock on onConfigurationChanged callback versus
+         * waitForInitialization.
+         */
+        Handler hwHandler = new Handler(mCallbackHandlerThread.getLooper());
+
+        RadioManager.ModuleProperties module = mModules.get(HARDCODED_MODULE_INDEX);
+        TunerCallbackAdapterExt cbExt = new TunerCallbackAdapterExt(callback, handler);
+
+        RadioTuner tuner = mRadioManager.openTuner(
+                module.getId(),
+                null,  // BandConfig - let the service automatically select one.
+                true,  // withAudio
+                cbExt, hwHandler);
+        mSessions.put(module.getId(), tuner);
+        if (tuner == null) return null;
+        RadioMetadataExt.setModuleId(module.getId());
+
+        if (module.isInitializationRequired()) {
+            if (!cbExt.waitForInitialization()) {
+                Log.w(TAG, "Timed out waiting for tuner initialization");
+                tuner.close();
+                return null;
+            }
+        }
+
+        return tuner;
+    }
+
+    public @Nullable List<BandDescriptor> getAmFmRegionConfig() {
+        initModules();
+        return mAmFmRegionConfig;
+    }
+
+    /* This won't be necessary when we push this code to the framework,
+     * as we really need only module references. */
+    private static Map<Integer, RadioTuner> mSessions = new HashMap<>();
+
+    public @Nullable Bitmap getMetadataImage(long globalId) {
+        if (globalId == 0) return null;
+
+        int moduleId = (int)(globalId >>> 32);
+        int localId = (int)(globalId & 0xFFFFFFFF);
+
+        RadioTuner tuner = mSessions.get(moduleId);
+        if (tuner == null) return null;
+
+        return tuner.getMetadataImage(localId);
+    }
+}
diff --git a/src/com/android/car/radio/platform/RadioTunerExt.java b/src/com/android/car/radio/platform/RadioTunerExt.java
new file mode 100644
index 0000000..c5b49c0
--- /dev/null
+++ b/src/com/android/car/radio/platform/RadioTunerExt.java
@@ -0,0 +1,136 @@
+/**
+ * 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.car.radio.platform;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.car.Car;
+import android.car.CarNotConnectedException;
+import android.car.media.CarAudioManager;
+import android.car.media.CarAudioPatchHandle;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ServiceConnection;
+import android.media.AudioAttributes;
+import android.os.IBinder;
+import android.util.Log;
+
+import java.util.stream.Stream;
+
+/**
+ * Proposed extensions to android.hardware.radio.RadioTuner.
+ *
+ * They might eventually get pushed to the framework.
+ */
+public class RadioTunerExt {
+    private static final String TAG = "BcRadioApp.tunerext";
+
+    // for now, we only support a single tuner with hardcoded address
+    private static final String HARDCODED_TUNER_ADDRESS = "tuner0";
+
+    private final Object mLock = new Object();
+    private final Car mCar;
+    @Nullable private CarAudioManager mCarAudioManager;
+
+    @Nullable private CarAudioPatchHandle mAudioPatch;
+    @Nullable private Boolean mPendingMuteOperation;
+
+    RadioTunerExt(Context context) {
+        mCar = Car.createCar(context, mCarServiceConnection);
+        mCar.connect();
+    }
+
+    private final ServiceConnection mCarServiceConnection = new ServiceConnection() {
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            synchronized (mLock) {
+                try {
+                    mCarAudioManager = (CarAudioManager)mCar.getCarManager(Car.AUDIO_SERVICE);
+                    if (mPendingMuteOperation != null) {
+                        boolean mute = mPendingMuteOperation;
+                        mPendingMuteOperation = null;
+                        Log.i(TAG, "Car connected, executing postponed operation: "
+                                + (mute ? "mute" : "unmute"));
+                        setMuted(mute);
+                    }
+                } catch (CarNotConnectedException e) {
+                    Log.e(TAG, "Car is not connected", e);
+                }
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            synchronized (mLock) {
+                mCarAudioManager = null;
+                mAudioPatch = null;
+            }
+        }
+    };
+
+    private boolean isSourceAvailableLocked(@NonNull String address)
+            throws CarNotConnectedException {
+        String[] sources = mCarAudioManager.getExternalSources();
+        return Stream.of(sources).anyMatch(source -> address.equals(source));
+    }
+
+    public boolean setMuted(boolean muted) {
+        synchronized (mLock) {
+            if (mCarAudioManager == null) {
+                Log.i(TAG, "Car not connected yet, postponing operation: "
+                        + (muted ? "mute" : "unmute"));
+                mPendingMuteOperation = muted;
+                return true;
+            }
+
+            // if it's already (not) muted - no need to (un)mute again
+            if ((mAudioPatch == null) == muted) return true;
+
+            try {
+                if (!muted) {
+                    if (!isSourceAvailableLocked(HARDCODED_TUNER_ADDRESS)) {
+                        Log.e(TAG, "Tuner source \"" + HARDCODED_TUNER_ADDRESS
+                                + "\" is not available");
+                        return false;
+                    }
+                    Log.d(TAG, "Creating audio patch for " + HARDCODED_TUNER_ADDRESS);
+                    mAudioPatch = mCarAudioManager.createAudioPatch(HARDCODED_TUNER_ADDRESS,
+                            AudioAttributes.USAGE_MEDIA, 0);
+                } else {
+                    Log.d(TAG, "Releasing audio patch");
+                    mCarAudioManager.releaseAudioPatch(mAudioPatch);
+                    mAudioPatch = null;
+                }
+                return true;
+            } catch (CarNotConnectedException e) {
+                Log.e(TAG, "Can't (un)mute - car is not connected", e);
+                return false;
+            }
+        }
+    }
+
+    public boolean isMuted() {
+        synchronized (mLock) {
+            if (mPendingMuteOperation != null) return mPendingMuteOperation;
+            return mAudioPatch == null;
+        }
+    }
+
+    public void close() {
+        mCar.disconnect();
+    }
+}
diff --git a/src/com/android/car/radio/platform/TunerCallbackAdapterExt.java b/src/com/android/car/radio/platform/TunerCallbackAdapterExt.java
new file mode 100644
index 0000000..471f4e3
--- /dev/null
+++ b/src/com/android/car/radio/platform/TunerCallbackAdapterExt.java
@@ -0,0 +1,129 @@
+/**
+ * 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.car.radio.platform;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager;
+import android.hardware.radio.RadioMetadata;
+import android.hardware.radio.RadioTuner;
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Proposed extensions to android.hardware.radio.TunerCallbackAdapter.
+ *
+ * This class is not compatible with the original at all (because they operate
+ * on different callback types), it just represents a proposed feature
+ * extensions.
+ *
+ * They might eventually get pushed to the framework.
+ */
+class TunerCallbackAdapterExt extends RadioTuner.Callback {
+    private static final int INIT_TIMEOUT_MS = 10000;  // 10s
+
+    private final Object mInitLock = new Object();
+    private boolean mIsInitialized = false;
+
+    private final RadioTuner.Callback mCallback;
+    private final Handler mHandler;
+
+    TunerCallbackAdapterExt(@NonNull RadioTuner.Callback callback, @Nullable Handler handler) {
+        mCallback = Objects.requireNonNull(callback);
+        if (handler == null) {
+            mHandler = new Handler(Looper.getMainLooper());
+        } else {
+            mHandler = handler;
+        }
+    }
+
+    public boolean waitForInitialization() {
+        synchronized (mInitLock) {
+            if (mIsInitialized) return true;
+            try {
+                mInitLock.wait(INIT_TIMEOUT_MS);
+            } catch (InterruptedException ex) {
+                // ignore the exception, as we check mIsInitialized anyway
+            }
+            return mIsInitialized;
+        }
+    }
+
+    @Override
+    public void onError(int status) {
+        mHandler.post(() -> mCallback.onError(status));
+    }
+
+    @Override
+    public void onTuneFailed(int result, @Nullable ProgramSelector selector) {
+        mHandler.post(() -> mCallback.onTuneFailed(result, selector));
+    }
+
+    @Override
+    public void onConfigurationChanged(RadioManager.BandConfig config) {
+        mHandler.post(() -> mCallback.onConfigurationChanged(config));
+        if (mIsInitialized) return;
+        synchronized (mInitLock) {
+            mIsInitialized = true;
+            mInitLock.notifyAll();
+        }
+    }
+
+    public void onProgramInfoChanged(RadioManager.ProgramInfo info) {
+        mHandler.post(() -> mCallback.onProgramInfoChanged(info));
+    }
+
+    public void onMetadataChanged(RadioMetadata metadata) {
+        mHandler.post(() -> mCallback.onMetadataChanged(metadata));
+    }
+
+    public void onTrafficAnnouncement(boolean active) {
+        mHandler.post(() -> mCallback.onTrafficAnnouncement(active));
+    }
+
+    public void onEmergencyAnnouncement(boolean active) {
+        mHandler.post(() -> mCallback.onEmergencyAnnouncement(active));
+    }
+
+    public void onAntennaState(boolean connected) {
+        mHandler.post(() -> mCallback.onAntennaState(connected));
+    }
+
+    public void onControlChanged(boolean control) {
+        mHandler.post(() -> mCallback.onControlChanged(control));
+    }
+
+    public void onBackgroundScanAvailabilityChange(boolean isAvailable) {
+        mHandler.post(() -> mCallback.onBackgroundScanAvailabilityChange(isAvailable));
+    }
+
+    public void onBackgroundScanComplete() {
+        mHandler.post(() -> mCallback.onBackgroundScanComplete());
+    }
+
+    public void onProgramListChanged() {
+        mHandler.post(() -> mCallback.onProgramListChanged());
+    }
+
+    public void onParametersUpdated(@NonNull Map<String, String> parameters) {
+        mHandler.post(() -> mCallback.onParametersUpdated(parameters));
+    }
+}
diff --git a/src/com/android/car/radio/service/IRadioCallback.aidl b/src/com/android/car/radio/service/IRadioCallback.aidl
new file mode 100644
index 0000000..d1ecf93
--- /dev/null
+++ b/src/com/android/car/radio/service/IRadioCallback.aidl
@@ -0,0 +1,47 @@
+/**
+ * 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.car.radio.service;
+
+import android.hardware.radio.RadioManager;
+
+/**
+ * Interface for applications to listen for changes in the current radio state.
+ */
+oneway interface IRadioCallback {
+    /**
+     * Called when the current program info has changed.
+     *
+     * This might happen either as a result of tune operation or just metadata change.
+     *
+     * @param info The current program info.
+     */
+    void onCurrentProgramInfoChanged(in RadioManager.ProgramInfo info);
+
+    /**
+     * Called when the mute state of the radio has changed.
+     *
+     * @param isMuted {@code true} if the radio is muted.
+     */
+    void onRadioMuteChanged(boolean isMuted);
+
+    /**
+     * Called when the radio has encountered an error.
+     *
+     * @param status One of the error states in {@link RadioManager}. For example,
+     *               {@link RadioManager#ERROR_HARDWARE_FAILURE}.
+     */
+    void onError(int status);
+}
diff --git a/src/com/android/car/radio/service/IRadioManager.aidl b/src/com/android/car/radio/service/IRadioManager.aidl
new file mode 100644
index 0000000..ba005ba
--- /dev/null
+++ b/src/com/android/car/radio/service/IRadioManager.aidl
@@ -0,0 +1,115 @@
+/**
+ * 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.car.radio.service;
+
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager;
+
+import com.android.car.broadcastradio.support.Program;
+import com.android.car.radio.service.IRadioCallback;
+
+/**
+ * Interface for apps to communicate with the radio.
+ */
+interface IRadioManager {
+    /**
+     * Tunes to a given program.
+     */
+    void tune(in ProgramSelector sel);
+
+    /**
+     * Seeks the radio forward.
+     */
+    void seekForward();
+
+    /**
+     * Seeks the radio backwards.
+     */
+    void seekBackward();
+
+    /**
+     * Mutes the radioN
+     *
+     * @return {@code true} if the mute was successful.
+     */
+    boolean mute();
+
+    /**
+     * Un-mutes the radio and causes audio to play.
+     *
+     * @return {@code true} if the un-mute was successful.
+     */
+    boolean unMute();
+
+    /**
+     * Returns {@code true} if the radio is currently muted.
+     */
+    boolean isMuted();
+
+    /**
+     * Adds new program to favorites list.
+     *
+     * @param favorite A program to add to favorites list.
+     */
+    void addFavorite(in Program favorite);
+
+    /**
+     * Removes a program from favorites list.
+     *
+     * @param sel ProgramSelector of a favorite to remove.
+     */
+    void removeFavorite(in ProgramSelector sel);
+
+    /**
+     * Switches radio band.
+     *
+     * Usually, this means tuning to the recently listened program on a given band.
+     *
+     * @param radioBand One of {@link RadioManager#BAND_FM}, {@link RadioManager#BAND_AM}.
+     */
+    void switchBand(int radioBand);
+
+    /**
+     * Adds the given {@link IRadioCallback} to be notified of any radio metadata changes.
+     */
+    void addRadioTunerCallback(in IRadioCallback callback);
+
+    /**
+     * Removes the given {@link IRadioCallback} from receiving any radio metadata chagnes.
+     */
+    void removeRadioTunerCallback(in IRadioCallback callback);
+
+    // TODO(b/73950974): use callback only (and make sure it's always called for new listeners)
+    RadioManager.ProgramInfo getCurrentProgramInfo();
+
+    /**
+     * Returns {@code true} if the radio was able to successfully initialize. A value of
+     * {@code false} here could mean that the {@code RadioService} was not able to connect to
+     * the {@link RadioManager} or there were no radio modules on the current device.
+     */
+    boolean isInitialized();
+
+    /**
+     * Returns {@code true} if the radio currently has focus and is therefore the application that
+     * is supplying music.
+     */
+    boolean hasFocus();
+
+    /**
+     * Returns a list of programs found with the tuner's background scan
+     */
+    List<RadioManager.ProgramInfo> getProgramList();
+}
diff --git a/src/com/android/car/radio/storage/Favorite.java b/src/com/android/car/radio/storage/Favorite.java
new file mode 100644
index 0000000..596099b
--- /dev/null
+++ b/src/com/android/car/radio/storage/Favorite.java
@@ -0,0 +1,63 @@
+/**
+ * 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.car.radio.storage;
+
+import android.arch.persistence.room.Embedded;
+import android.arch.persistence.room.Entity;
+import android.arch.persistence.room.PrimaryKey;
+import android.hardware.radio.ProgramSelector;
+import android.support.annotation.NonNull;
+
+import com.android.car.broadcastradio.support.Program;
+
+import java.util.Objects;
+
+@Entity
+class Favorite {
+    @PrimaryKey
+    @NonNull
+    @Embedded(prefix = "primaryId_")
+    public final IdentifierEntity primaryId;
+
+    @NonNull
+    public final ProgramSelector selector;
+
+    @NonNull
+    public final String name;
+
+    Favorite(@NonNull IdentifierEntity primaryId, @NonNull ProgramSelector selector,
+            @NonNull String name) {
+        if (!primaryId.sameAs(selector.getPrimaryId())) {
+            throw new IllegalArgumentException(
+                    "Can't set different primary ID than program selector's");
+        }
+
+        this.primaryId = primaryId;
+        this.selector = selector;
+        this.name = Objects.requireNonNull(name);
+    }
+
+    Favorite(@NonNull Program program) {
+        this(new IdentifierEntity(program.getSelector().getPrimaryId()),
+                program.getSelector(), program.getName());
+    }
+
+    @NonNull
+    public Program toProgram() {
+        return new Program(selector, name);
+    }
+}
diff --git a/src/com/android/car/radio/storage/IdentifierEntity.java b/src/com/android/car/radio/storage/IdentifierEntity.java
new file mode 100644
index 0000000..ccda8ba
--- /dev/null
+++ b/src/com/android/car/radio/storage/IdentifierEntity.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.car.radio.storage;
+
+import android.annotation.Nullable;
+import android.arch.persistence.room.Entity;
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.ProgramSelector.IdentifierType;
+import android.support.annotation.NonNull;
+
+@Entity
+class IdentifierEntity {
+    @IdentifierType
+    public final int type;
+
+    public final long value;
+
+    IdentifierEntity(@IdentifierType int type, long value) {
+        this.type = type;
+        this.value = value;
+    }
+
+    IdentifierEntity(@NonNull ProgramSelector.Identifier id) {
+        type = id.getType();
+        value = id.getValue();
+    }
+
+    public boolean sameAs(@Nullable ProgramSelector.Identifier id) {
+        if (id == null) return false;
+        return id.getType() == type && id.getValue() == value;
+    }
+}
diff --git a/src/com/android/car/radio/storage/ProgramSelectorConverter.java b/src/com/android/car/radio/storage/ProgramSelectorConverter.java
new file mode 100644
index 0000000..a41a527
--- /dev/null
+++ b/src/com/android/car/radio/storage/ProgramSelectorConverter.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.car.radio.storage;
+
+import android.arch.persistence.room.TypeConverter;
+import android.hardware.radio.ProgramSelector;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+
+import com.android.car.broadcastradio.support.platform.ProgramSelectorExt;
+
+import java.util.Objects;
+
+class ProgramSelectorConverter {
+    @TypeConverter
+    @NonNull
+    public static ProgramSelector toSelector(@NonNull String uri) {
+        return Objects.requireNonNull(ProgramSelectorExt.fromUri(Uri.parse(uri)));
+    }
+
+    @TypeConverter
+    @NonNull
+    public static String toString(@NonNull ProgramSelector sel) {
+        return ProgramSelectorExt.toUri(sel).toString();
+    }
+}
diff --git a/src/com/android/car/radio/storage/RadioDatabase.java b/src/com/android/car/radio/storage/RadioDatabase.java
new file mode 100644
index 0000000..29e9078
--- /dev/null
+++ b/src/com/android/car/radio/storage/RadioDatabase.java
@@ -0,0 +1,100 @@
+/**
+ * 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.car.radio.storage;
+
+import android.annotation.WorkerThread;
+import android.arch.lifecycle.LiveData;
+import android.arch.lifecycle.Transformations;
+import android.arch.persistence.room.Dao;
+import android.arch.persistence.room.Database;
+import android.arch.persistence.room.Insert;
+import android.arch.persistence.room.OnConflictStrategy;
+import android.arch.persistence.room.Query;
+import android.arch.persistence.room.Room;
+import android.arch.persistence.room.RoomDatabase;
+import android.arch.persistence.room.TypeConverters;
+import android.content.Context;
+import android.hardware.radio.ProgramSelector;
+import android.support.annotation.NonNull;
+
+import com.android.car.broadcastradio.support.Program;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Radio app database schema.
+ *
+ * This class should not be accessed directly.
+ * Instead, {@link RadioStorage} interfaces directly with it.
+ */
+@Database(entities = {Favorite.class}, exportSchema = false, version = 1)
+@TypeConverters({ProgramSelectorConverter.class})
+abstract class RadioDatabase extends RoomDatabase {
+    @Dao
+    protected interface FavoriteDao {
+        @Query("SELECT * FROM Favorite ORDER BY primaryId_type, primaryId_value")
+        LiveData<List<Favorite>> loadAll();
+
+        @Insert(onConflict = OnConflictStrategy.REPLACE)
+        void insertAll(Favorite... favorites);
+
+        @Query("DELETE FROM Favorite WHERE "
+                + "primaryId_type = :primaryIdType AND primaryId_value = :primaryIdValue")
+        void delete(int primaryIdType, long primaryIdValue);
+
+        default void delete(@NonNull ProgramSelector.Identifier id) {
+            delete(id.getType(), id.getValue());
+        }
+    }
+
+    protected abstract FavoriteDao favoriteDao();
+
+    public static RadioDatabase buildInstance(Context context) {
+        return Room.databaseBuilder(context.getApplicationContext(),
+                RadioDatabase.class, RadioDatabase.class.getSimpleName()).build();
+    }
+
+    /**
+     * Returns a list of all user stored radio favorites sorted by primary identifier.
+     */
+    @WorkerThread
+    @NonNull
+    public LiveData<List<Program>> getAllFavorites() {
+        return Transformations.map(favoriteDao().loadAll(), favorites ->
+                favorites.stream().map(Favorite::toProgram).collect(Collectors.toList()));
+    }
+
+    /**
+     * Saves a given {@link Program} as a favorite.
+     *
+     * The favorite will replace any existing entry for a given primary
+     * identifier if there is a conflict.
+     */
+    @WorkerThread
+    public void insertFavorite(@NonNull Program preset) {
+        favoriteDao().insertAll(new Favorite(preset));
+    }
+
+    /**
+     * Removes a favorite by primary id of its {@link ProgramSelector}.
+     */
+    @WorkerThread
+    public void removeFavorite(@NonNull ProgramSelector sel) {
+        favoriteDao().delete(sel.getPrimaryId());
+    }
+}
diff --git a/src/com/android/car/radio/storage/RadioStorage.java b/src/com/android/car/radio/storage/RadioStorage.java
new file mode 100644
index 0000000..b77a64c
--- /dev/null
+++ b/src/com/android/car/radio/storage/RadioStorage.java
@@ -0,0 +1,253 @@
+/*
+ * 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.car.radio.storage;
+
+import android.annotation.NonNull;
+import android.arch.lifecycle.LiveData;
+import android.arch.lifecycle.Observer;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager;
+import android.os.AsyncTask;
+import android.util.Log;
+
+import com.android.car.broadcastradio.support.Program;
+import com.android.car.radio.utils.ProgramSelectorUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Class that manages persistent storage of various radio options.
+ */
+public class RadioStorage {
+    private static final String TAG = "Em.RadioStorage";
+    private static final String PREF_NAME = "com.android.car.radio.RadioStorage";
+
+    // Keys used for storage in the SharedPreferences.
+    private static final String PREF_KEY_RADIO_BAND = "radio_band";
+    private static final String PREF_KEY_RADIO_CHANNEL_AM = "radio_channel_am";
+    private static final String PREF_KEY_RADIO_CHANNEL_FM = "radio_channel_fm";
+
+    public static final int INVALID_RADIO_CHANNEL = -1;
+    public static final int INVALID_RADIO_BAND = -1;
+
+    private static SharedPreferences sSharedPref;
+    private static RadioStorage sInstance;
+    private static RadioDatabase sRadioDatabase;
+
+    /**
+     * Listener that will be called when something in the radio storage changes.
+     */
+    public interface PresetsChangeListener {
+        /**
+         * Called when favorite list has changed.
+         */
+        void onPresetsRefreshed();
+    }
+
+    private final LiveData<List<Program>> mFavorites;
+
+    // TODO(b/73950974): use Observer<> directly
+    private final Map<PresetsChangeListener, Observer<List<Program>>> mPresetListeners =
+            new HashMap<>();
+
+    private RadioStorage(Context context) {
+        if (sSharedPref == null) {
+            sSharedPref = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
+        }
+
+        if (sRadioDatabase == null) {
+            sRadioDatabase = RadioDatabase.buildInstance(context);
+        }
+
+        mFavorites = sRadioDatabase.getAllFavorites();
+    }
+
+    /**
+     * Returns singleton instance of {@link RadioStorage}.
+     */
+    public static RadioStorage getInstance(Context context) {
+        if (sInstance == null) {
+            sInstance = new RadioStorage(context.getApplicationContext());
+        }
+
+        return sInstance;
+    }
+
+    /**
+     * Registers the given {@link PresetsChangeListener} to be notified when any radio preset state
+     * has changed.
+     */
+    public void addPresetsChangeListener(PresetsChangeListener listener) {
+        Observer<List<Program>> observer = list -> listener.onPresetsRefreshed();
+        synchronized (mPresetListeners) {
+            mFavorites.observeForever(observer);
+            mPresetListeners.put(listener, observer);
+        }
+    }
+
+    /**
+     * Unregisters the given {@link PresetsChangeListener}.
+     */
+    public void removePresetsChangeListener(PresetsChangeListener listener) {
+        Observer<List<Program>> observer;
+        synchronized (mPresetListeners) {
+            observer = mPresetListeners.remove(listener);
+            mFavorites.removeObserver(observer);
+        }
+    }
+
+    /**
+     * Returns all currently loaded presets. If there are no stored presets, this method will
+     * return an empty {@link List}.
+     *
+     * <p>Register as a {@link PresetsChangeListener} to be notified of any changes in the
+     * preset list.
+     */
+    public @NonNull List<Program> getPresets() {
+        List<Program> favorites = mFavorites.getValue();
+        if (favorites != null) return favorites;
+
+        // It won't be a problem when we use Observer<> directly.
+        Log.w(TAG, "Database is not ready yet");
+        return new ArrayList<>();
+    }
+
+    /**
+     * Returns {@code true} if the given {@link ProgramSelector} is a user saved favorite.
+     */
+    public boolean isPreset(@NonNull ProgramSelector selector) {
+        return mFavorites.getValue().contains(new Program(selector, ""));
+    }
+
+    /**
+     * Stores that given {@link Program} as a preset. This operation will override any
+     * previously stored preset that matches the given preset.
+     *
+     * <p>Upon a successful store, the presets list will be refreshed via a call to
+     * {@link #refreshPresets()}.
+     *
+     * @see #refreshPresets()
+     */
+    public void storePreset(@NonNull Program preset) {
+        new StorePresetAsyncTask().execute(Objects.requireNonNull(preset));
+    }
+
+    /**
+     * Removes the given {@link Program} as a preset.
+     *
+     * <p>Upon a successful removal, the presets list will be refreshed via a call to
+     * {@link #refreshPresets()}.
+     *
+     * @see #refreshPresets()
+     */
+    public void removePreset(@NonNull ProgramSelector preset) {
+        new RemovePresetAsyncTask().execute(Objects.requireNonNull(preset));
+    }
+
+    /**
+     * Returns the stored radio band that was set in {@link #storeRadioChannel}. If a radio band
+     * has not previously been stored, then {@link RadioManager#BAND_FM} is returned.
+     *
+     * @return One of {@link RadioManager#BAND_FM} or {@link RadioManager#BAND_AM}.
+     */
+    public int getStoredRadioBand() {
+        return sSharedPref.getInt(PREF_KEY_RADIO_BAND, RadioManager.BAND_FM);
+    }
+
+    /**
+     * Returns the stored radio channel that was set in {@link #storeRadioChannel(int, int)}. If a
+     * radio channel for the given band has not been previously stored, then
+     * {@link #INVALID_RADIO_CHANNEL} is returned.
+     *
+     * @param band One of the BAND_* values from {@link RadioManager}. For example,
+     *             {@link RadioManager#BAND_AM}.
+     */
+    public long getStoredRadioChannel(int band) {
+        switch (band) {
+            case RadioManager.BAND_AM:
+                return sSharedPref.getLong(PREF_KEY_RADIO_CHANNEL_AM, INVALID_RADIO_CHANNEL);
+
+            case RadioManager.BAND_FM:
+                return sSharedPref.getLong(PREF_KEY_RADIO_CHANNEL_FM, INVALID_RADIO_CHANNEL);
+
+            default:
+                return INVALID_RADIO_CHANNEL;
+        }
+    }
+
+    /**
+     * Stores a radio channel (i.e. the radio frequency) for a particular band so it can be later
+     * retrieved via {@link #getStoredRadioChannel(int band)}.
+     */
+    public void storeRadioChannel(@NonNull ProgramSelector sel) {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "storeRadioChannel(" + sel + ")");
+        }
+
+        // TODO(b/73950974): don't store if it's already the same
+
+        int band = ProgramSelectorUtils.getRadioBand(sel);
+        if (band != RadioManager.BAND_AM && band != RadioManager.BAND_FM) return;
+
+        SharedPreferences.Editor editor = sSharedPref.edit();
+        editor.putInt(PREF_KEY_RADIO_BAND, band);
+
+        long freq = sel.getFirstId(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY);
+        if (band == RadioManager.BAND_AM) {
+            editor.putLong(PREF_KEY_RADIO_CHANNEL_AM, freq);
+        }
+        if (band == RadioManager.BAND_FM) {
+            editor.putLong(PREF_KEY_RADIO_CHANNEL_FM, freq);
+        }
+
+        editor.apply();
+    }
+
+    /**
+     * {@link AsyncTask} that will store a single {@link Program} that is passed to its
+     * {@link AsyncTask#execute(Object[])}.
+     */
+    private class StorePresetAsyncTask extends AsyncTask<Program, Void, Boolean> {
+        private static final String TAG = "Em.StorePresetAT";
+
+        @Override
+        protected Boolean doInBackground(Program... programs) {
+            sRadioDatabase.insertFavorite(programs[0]);
+            return true;
+        }
+    }
+
+    /**
+     * {@link AsyncTask} that will remove a single {@link Program} that is passed to its
+     * {@link AsyncTask#execute(Object[])}.
+     */
+    private class RemovePresetAsyncTask extends AsyncTask<ProgramSelector, Void, Boolean> {
+        private static final String TAG = "Em.RemovePresetAT";
+
+        @Override
+        protected Boolean doInBackground(ProgramSelector... selectors) {
+            sRadioDatabase.removeFavorite(selectors[0]);
+            return true;
+        }
+    }
+}
diff --git a/src/com/android/car/radio/utils/ProgramSelectorUtils.java b/src/com/android/car/radio/utils/ProgramSelectorUtils.java
new file mode 100644
index 0000000..c1d10f4
--- /dev/null
+++ b/src/com/android/car/radio/utils/ProgramSelectorUtils.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.radio.utils;
+
+import android.annotation.NonNull;
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager;
+
+import com.android.car.broadcastradio.support.platform.ProgramSelectorExt;
+import com.android.car.radio.storage.RadioStorage;
+
+/**
+ * Helper methods for android.hardware.radio.ProgramSelector.
+ *
+ * Most probably, this code will end up refactored out, less likely pushed to the platform.
+ */
+public class ProgramSelectorUtils {
+    /**
+     * Returns AM/FM band for a given ProgramSelector.
+     *
+     * @param sel ProgramSelector to check.
+     * @return {@link RadioManager#BAND_FM} if sel is FM program,
+     *         {@link RadioManager#BAND_AM} if sel is AM program,
+     *         {@link RadioStorage#INVALID_RADIO_BAND} otherwise.
+     */
+    public static int getRadioBand(@NonNull ProgramSelector sel) {
+        if (!ProgramSelectorExt.isAmFmProgram(sel)) return RadioStorage.INVALID_RADIO_BAND;
+        if (!ProgramSelectorExt.hasId(sel, ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)) {
+            return RadioStorage.INVALID_RADIO_BAND;
+        }
+
+        long freq = sel.getFirstId(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY);
+        if (ProgramSelectorExt.isAmFrequency(freq)) return RadioManager.BAND_AM;
+        if (ProgramSelectorExt.isFmFrequency(freq)) return RadioManager.BAND_FM;
+
+        return RadioStorage.INVALID_RADIO_BAND;
+    }
+}
diff --git a/src/com/android/car/radio/utils/ThrowingRunnable.java b/src/com/android/car/radio/utils/ThrowingRunnable.java
new file mode 100644
index 0000000..12a19fe
--- /dev/null
+++ b/src/com/android/car/radio/utils/ThrowingRunnable.java
@@ -0,0 +1,21 @@
+/**
+ * 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.car.radio.utils;
+
+public interface ThrowingRunnable<E extends Exception> {
+    void run() throws E;
+}