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;
+}