summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2019-09-04 13:34:24 -0700
committerXin Li <delphij@google.com>2019-09-04 13:34:24 -0700
commitb69a29c0a201dbffaaa2e748f1a00c69072a4b02 (patch)
tree0259ace16acf446bd6af793ac9db79fa5ca9dd3e
parente5edb67e9c17e1cb272874a1a4c33a7812ae96d7 (diff)
parent3010ed4f62cab83463e235e5d5ed6d19e95cfb65 (diff)
downloadMedia-b69a29c0a201dbffaaa2e748f1a00c69072a4b02.tar.gz
DO NOT MERGE - Merge Android 10 into masterndk-sysroot-r21
Bug: 139893257 Change-Id: If2b62160845364588a0932632b59ebac41a78f57
-rw-r--r--Android.mk16
-rw-r--r--AndroidManifest.xml22
-rw-r--r--res/drawable-hdpi/ic_list_view_disable.pngbin2033 -> 0 bytes
-rw-r--r--res/drawable-hdpi/ic_music_active.pngbin776 -> 0 bytes
-rw-r--r--res/drawable-hdpi/progressbar.9.pngbin139 -> 0 bytes
-rw-r--r--res/drawable-mdpi/ic_list_view_disable.pngbin1739 -> 0 bytes
-rw-r--r--res/drawable-mdpi/ic_music_active.pngbin697 -> 0 bytes
-rw-r--r--res/drawable-mdpi/progressbar.9.pngbin130 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/ic_list_view_disable.pngbin2697 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/ic_music_active.pngbin856 -> 0 bytes
-rw-r--r--res/drawable-xhdpi/progressbar.9.pngbin155 -> 0 bytes
-rw-r--r--res/drawable-xxhdpi/ic_list_view_disable.pngbin1502 -> 0 bytes
-rw-r--r--res/drawable-xxhdpi/ic_music_active.pngbin347 -> 0 bytes
-rw-r--r--res/drawable-xxhdpi/progressbar.9.pngbin1057 -> 0 bytes
-rw-r--r--res/drawable/browse_playback_background.xml (renamed from res/drawable/app_item_background.xml)22
-rw-r--r--res/drawable/grid_item_background.xml26
-rw-r--r--res/drawable/ic_arrow_drop_down.xml25
-rw-r--r--res/drawable/ic_arrow_drop_up.xml25
-rw-r--r--res/drawable/ic_chevron_left.xml25
-rw-r--r--res/drawable/ic_chevron_right.xml6
-rw-r--r--res/drawable/ic_equalizer.xml (renamed from res/values-h1200dp/dimens.xml)15
-rw-r--r--res/drawable/ic_explicit_black.xml12
-rw-r--r--res/drawable/ic_file_download_done_black.xml12
-rw-r--r--res/drawable/ic_music.xml31
-rw-r--r--res/drawable/ic_queue_button.xml (renamed from res/drawable/music_action_background.xml)13
-rw-r--r--res/drawable/ic_search.xml25
-rw-r--r--res/drawable/ic_settings.xml34
-rw-r--r--res/drawable/ic_tracklist.xml30
-rw-r--r--res/drawable/media_app_title_background.xml17
-rw-r--r--res/drawable/seekbar_background.xml13
-rw-r--r--res/drawable/seekbar_thumb.xml7
-rw-r--r--res/layout-h1200dp/fragment_metadata.xml65
-rw-r--r--res/layout-h1200dp/fragment_metadata_with_queue.xml62
-rw-r--r--res/layout-port/metadata_normal.xml87
-rw-r--r--res/layout/app_selection_item.xml48
-rw-r--r--res/layout/appbar_view.xml122
-rw-r--r--res/layout/browse_state.xml35
-rw-r--r--res/layout/fragment_app_selection.xml37
-rw-r--r--res/layout/fragment_browse.xml8
-rw-r--r--res/layout/fragment_empty.xml3
-rw-r--r--res/layout/fragment_error.xml (renamed from res/layout-h1200dp/fragment_playback_with_queue.xml)45
-rw-r--r--res/layout/fragment_metadata.xml64
-rw-r--r--res/layout/fragment_playback.xml160
-rw-r--r--res/layout/fragment_search.xml (renamed from res/layout/media_browse_more_footer.xml)32
-rw-r--r--res/layout/media_activity.xml76
-rw-r--r--res/layout/media_browse_grid_item.xml85
-rw-r--r--res/layout/media_browse_header_item.xml12
-rw-r--r--res/layout/media_browse_list_item.xml134
-rw-r--r--res/layout/media_browse_panel_item.xml56
-rw-r--r--res/layout/media_browse_spacer.xml21
-rw-r--r--res/layout/metadata_compact.xml58
-rw-r--r--res/layout/metadata_normal.xml97
-rw-r--r--res/layout/playback_controls.xml26
-rw-r--r--res/layout/queue_list_item.xml78
-rw-r--r--res/layout/search_bar.xml52
-rw-r--r--res/layout/tab_view.xml40
-rw-r--r--res/layout/time_progress_text.xml52
-rw-r--r--res/transition/queue_in.xml34
-rw-r--r--res/transition/queue_out.xml34
-rw-r--r--res/values-night/dimens.xml (renamed from res/values-wheel/bools.xml)5
-rw-r--r--res/values-port/dimens.xml29
-rw-r--r--res/values-port/integers.xml (renamed from res/values-h1200dp/bools.xml)7
-rw-r--r--res/values-port/styles.xml (renamed from res/values/attrs.xml)17
-rw-r--r--res/values-w1024dp/dimens.xml41
-rw-r--r--res/values-w1280dp/dimens.xml23
-rw-r--r--res/values-w1280dp/integers.xml (renamed from res/values-w748dp/dimens.xml)6
-rw-r--r--res/values-w1280dp/styles.xml29
-rw-r--r--res/values/bools.xml12
-rw-r--r--res/values/colors.xml38
-rw-r--r--res/values/dimens.xml137
-rw-r--r--res/values/integers.xml21
-rw-r--r--res/values/strings.xml18
-rw-r--r--res/values/strings_no_translation.xml (renamed from res/values-w768dp/dimens.xml)13
-rw-r--r--res/values/styles.xml78
-rw-r--r--res/values/themes.xml11
-rw-r--r--src/com/android/car/media/AppSelectionFragment.java152
-rw-r--r--src/com/android/car/media/BrowseFragment.java373
-rw-r--r--src/com/android/car/media/EmptyFragment.java73
-rw-r--r--src/com/android/car/media/ErrorFragment.java90
-rw-r--r--src/com/android/car/media/MediaActivity.java972
-rw-r--r--src/com/android/car/media/MediaConstants.java102
-rw-r--r--src/com/android/car/media/MediaDispatcherActivity.java62
-rw-r--r--src/com/android/car/media/MediaManager.java597
-rw-r--r--src/com/android/car/media/MediaPlaybackModel.java409
-rw-r--r--src/com/android/car/media/MetadataController.java177
-rw-r--r--src/com/android/car/media/PlaybackFragment.java565
-rw-r--r--src/com/android/car/media/browse/BrowseAdapter.java554
-rw-r--r--src/com/android/car/media/browse/BrowseItemViewType.java20
-rw-r--r--src/com/android/car/media/browse/BrowseViewData.java32
-rw-r--r--src/com/android/car/media/browse/BrowseViewHolder.java47
-rw-r--r--src/com/android/car/media/browse/ContentForwardStrategy.java128
-rw-r--r--src/com/android/car/media/drawer/MediaBrowserItemsFetcher.java199
-rw-r--r--src/com/android/car/media/drawer/MediaDrawerAdapter.java160
-rw-r--r--src/com/android/car/media/drawer/MediaDrawerController.java227
-rw-r--r--src/com/android/car/media/drawer/MediaItemOnClickListener.java40
-rw-r--r--src/com/android/car/media/drawer/MediaItemsFetcher.java141
-rw-r--r--src/com/android/car/media/drawer/MediaQueueItemsFetcher.java157
-rw-r--r--src/com/android/car/media/widgets/AppBarView.java300
-rw-r--r--src/com/android/car/media/widgets/MediaItemTab.java52
-rw-r--r--src/com/android/car/media/widgets/MediaItemTabView.java71
-rw-r--r--src/com/android/car/media/widgets/MetadataView.java54
-rw-r--r--src/com/android/car/media/widgets/SearchBar.java132
-rw-r--r--src/com/android/car/media/widgets/ViewUtils.java64
103 files changed, 3220 insertions, 5214 deletions
diff --git a/Android.mk b/Android.mk
index ece3138..22453ab 100644
--- a/Android.mk
+++ b/Android.mk
@@ -23,8 +23,11 @@ LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
LOCAL_PACKAGE_NAME := CarMediaApp
+
LOCAL_PRIVATE_PLATFORM_APIS := true
+LOCAL_REQUIRED_MODULES := privapp_whitelist_com.android.car.media
+
LOCAL_CERTIFICATE := platform
LOCAL_MODULE_TAGS := optional
@@ -33,21 +36,26 @@ LOCAL_PRIVILEGED_MODULE := true
LOCAL_USE_AAPT2 := true
+LOCAL_JAVA_LIBRARIES += android.car
+
LOCAL_PROGUARD_ENABLED := disabled
LOCAL_DEX_PREOPT := false
LOCAL_JAVA_LIBRARIES += android.car
+LOCAL_STATIC_JAVA_LIBRARIES += \
+ androidx-constraintlayout_constraintlayout-solver
+
LOCAL_STATIC_ANDROID_LIBRARIES += \
- androidx.car_car \
androidx-constraintlayout_constraintlayout \
- androidx.design_design \
car-apps-common \
car-media-common
-LOCAL_STATIC_JAVA_LIBRARIES += \
- androidx-constraintlayout_constraintlayout-solver
+# Including the resources for the static android libraries allows to pick up their static overlays.
+LOCAL_RESOURCE_DIR += \
+ $(LOCAL_PATH)/../libs/car-apps-common/res \
+ $(LOCAL_PATH)/../libs/car-media-common/res
include $(BUILD_PACKAGE)
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 76e2464..d354560 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -16,19 +16,16 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- android:sharedUserId="com.android.car.media"
- package="com.android.car.media" >
+ package="com.android.car.media"
+ android:sharedUserId="android.uid.system">
<uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
- <uses-sdk
- android:minSdkVersion="24"
- android:targetSdkVersion='24'/>
-
<application
- android:label="CarMediaApp"
+ android:label="Media Center"
+ android:theme="@style/Theme.Media"
android:icon="@drawable/ic_music">
<meta-data
@@ -37,13 +34,20 @@
<activity
android:name=".MediaActivity"
- android:theme="@style/MediaActivityTheme"
android:resizeableActivity="true"
+ android:windowSoftInputMode="stateUnchanged|adjustPan"
android:launchMode="singleTop">
+ <meta-data android:name="distractionOptimized" android:value="true"/>
+ </activity>
+
+ <!-- The Media center entry point that trampolines into MediaActivity or the radio app. -->
+ <activity
+ android:name=".MediaDispatcherActivity"
+ android:launchMode="singleTask">
+ <meta-data android:name="distractionOptimized" android:value="true"/>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.INFO" />
- <category android:name="android.intent.category.APP_MUSIC" />
</intent-filter>
<intent-filter>
<action android:name="android.car.intent.action.MEDIA_TEMPLATE" />
diff --git a/res/drawable-hdpi/ic_list_view_disable.png b/res/drawable-hdpi/ic_list_view_disable.png
deleted file mode 100644
index 651ee62..0000000
--- a/res/drawable-hdpi/ic_list_view_disable.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/ic_music_active.png b/res/drawable-hdpi/ic_music_active.png
deleted file mode 100644
index ca701de..0000000
--- a/res/drawable-hdpi/ic_music_active.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/progressbar.9.png b/res/drawable-hdpi/progressbar.9.png
deleted file mode 100644
index 784de64..0000000
--- a/res/drawable-hdpi/progressbar.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/ic_list_view_disable.png b/res/drawable-mdpi/ic_list_view_disable.png
deleted file mode 100644
index 8de7968..0000000
--- a/res/drawable-mdpi/ic_list_view_disable.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/ic_music_active.png b/res/drawable-mdpi/ic_music_active.png
deleted file mode 100644
index 1f583b4..0000000
--- a/res/drawable-mdpi/ic_music_active.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/progressbar.9.png b/res/drawable-mdpi/progressbar.9.png
deleted file mode 100644
index 268a32c..0000000
--- a/res/drawable-mdpi/progressbar.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_list_view_disable.png b/res/drawable-xhdpi/ic_list_view_disable.png
deleted file mode 100644
index 82adcb2..0000000
--- a/res/drawable-xhdpi/ic_list_view_disable.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_music_active.png b/res/drawable-xhdpi/ic_music_active.png
deleted file mode 100644
index e31e4d0..0000000
--- a/res/drawable-xhdpi/ic_music_active.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/progressbar.9.png b/res/drawable-xhdpi/progressbar.9.png
deleted file mode 100644
index b0b3442..0000000
--- a/res/drawable-xhdpi/progressbar.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_list_view_disable.png b/res/drawable-xxhdpi/ic_list_view_disable.png
deleted file mode 100644
index fc64935..0000000
--- a/res/drawable-xxhdpi/ic_list_view_disable.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_music_active.png b/res/drawable-xxhdpi/ic_music_active.png
deleted file mode 100644
index 9026a8e..0000000
--- a/res/drawable-xxhdpi/ic_music_active.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/progressbar.9.png b/res/drawable-xxhdpi/progressbar.9.png
deleted file mode 100644
index f78c915..0000000
--- a/res/drawable-xxhdpi/progressbar.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/app_item_background.xml b/res/drawable/browse_playback_background.xml
index 605c032..e1f3432 100644
--- a/res/drawable/app_item_background.xml
+++ b/res/drawable/browse_playback_background.xml
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~
- ~ Copyright (C) 2018 Google Inc.
+ ~ Copyright 2018 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
@@ -14,14 +13,11 @@
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
- ~
- -->
-<ripple xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/app_item_background_color">
- <item android:id="@android:id/mask">
- <shape android:shape="rectangle">
- <solid android:color="?android:colorAccent" />
- <corners android:radius="@dimen/car_radius_2"/>
- </shape>
- </item>
-</ripple>
+ -->
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+
+ <corners android:radius="@dimen/browse_playback_background_radius" />
+ <solid android:color="@color/browse_playback_bg_color" />
+</shape> \ No newline at end of file
diff --git a/res/drawable/grid_item_background.xml b/res/drawable/grid_item_background.xml
new file mode 100644
index 0000000..2a2b4fe
--- /dev/null
+++ b/res/drawable/grid_item_background.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<ripple
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@*android:color/car_card_ripple_background">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <solid android:color="@*android:color/white"/>
+ <corners android:radius="@dimen/media_browse_grid_item_background_radius"/>
+ </shape>
+ </item>
+</ripple> \ No newline at end of file
diff --git a/res/drawable/ic_arrow_drop_down.xml b/res/drawable/ic_arrow_drop_down.xml
deleted file mode 100644
index 2774d0f..0000000
--- a/res/drawable/ic_arrow_drop_down.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2018, The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0">
- <path
- android:fillColor="#FF000000"
- android:pathData="M7,10l5,5 5,-5z"/>
-</vector>
diff --git a/res/drawable/ic_arrow_drop_up.xml b/res/drawable/ic_arrow_drop_up.xml
deleted file mode 100644
index 1ba2974..0000000
--- a/res/drawable/ic_arrow_drop_up.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2018, The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0">
- <path
- android:fillColor="#FF000000"
- android:pathData="M7,14l5,-5 5,5z"/>
-</vector>
diff --git a/res/drawable/ic_chevron_left.xml b/res/drawable/ic_chevron_left.xml
deleted file mode 100644
index 42e9cbc..0000000
--- a/res/drawable/ic_chevron_left.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2018, The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0">
- <path
- android:fillColor="#FF000000"
- android:pathData="M15.41,7.41L14,6l-6,6 6,6 1.41,-1.41L10.83,12z"/>
-</vector>
diff --git a/res/drawable/ic_chevron_right.xml b/res/drawable/ic_chevron_right.xml
index dc9d6db..f626a17 100644
--- a/res/drawable/ic_chevron_right.xml
+++ b/res/drawable/ic_chevron_right.xml
@@ -1,12 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 The Android Open Source Project
-
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
-
http://www.apache.org/licenses/LICENSE-2.0
-
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -18,9 +15,8 @@
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
-
<path
- android:fillColor="#212121"
+ android:fillColor="#cecece"
android:strokeWidth="1"
android:pathData="M 10 6 L 8.59 7.41 L 13.17 12 L 8.59 16.59 L 10 18 L 16 12 Z" />
<path
diff --git a/res/values-h1200dp/dimens.xml b/res/drawable/ic_equalizer.xml
index 6032220..af21358 100644
--- a/res/values-h1200dp/dimens.xml
+++ b/res/drawable/ic_equalizer.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
+<!-- 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.
@@ -13,7 +13,12 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
-<resources>
- <!-- Application Bar -->
- <dimen name="car_app_bar_height">116dp</dimen>
-</resources>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="36dp"
+ android:height="36dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path
+ android:fillColor="@color/queue_playing_icon_color"
+ android:pathData="M20,40h8L28,8h-8v32zM8,40h8L16,24L8,24v16zM32,18v22h8L40,18h-8z" />
+</vector>
diff --git a/res/drawable/ic_explicit_black.xml b/res/drawable/ic_explicit_black.xml
new file mode 100644
index 0000000..10c2b43
--- /dev/null
+++ b/res/drawable/ic_explicit_black.xml
@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:pathData="M19,3L5,3C3.9,3 3,3.9 3,5L3,19C3,20.1 3.9,21 5,21L19,21C20.1,21 21,20.1 21,19L21,5C21,3.9 20.1,3 19,3ZM15,9L11,9L11,11L15,11L15,13L11,13L11,15L15,15L15,17L9,17L9,7L15,7L15,9Z"
+ android:strokeColor="#00000000"
+ android:fillType="nonZero"
+ android:fillColor="#000000"
+ android:strokeWidth="1"/>
+</vector>
diff --git a/res/drawable/ic_file_download_done_black.xml b/res/drawable/ic_file_download_done_black.xml
new file mode 100644
index 0000000..c5e039a
--- /dev/null
+++ b/res/drawable/ic_file_download_done_black.xml
@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:pathData="M5,18L19,18L19,20L5,20L5,18ZM9.6,15.3L5,10.7L7,8.8L9.6,11.4L17,4L19,6L9.6,15.3Z"
+ android:strokeColor="#00000000"
+ android:fillType="nonZero"
+ android:fillColor="#000000"
+ android:strokeWidth="1"/>
+</vector>
diff --git a/res/drawable/ic_music.xml b/res/drawable/ic_music.xml
deleted file mode 100644
index f16dc7f..0000000
--- a/res/drawable/ic_music.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2018, The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="48dp"
- android:height="48dp"
- android:viewportWidth="48"
- android:viewportHeight="48">
-
- <path
- android:fillAlpha=".1"
- android:strokeAlpha=".1"
- android:pathData="M0 0h48v48H0z" />
- <path
- android:fillColor="#FFFFFFFF"
- android:pathData="M24 2C14.06 2 6 10.06 6 20v14c0 3.31 2.69 6 6 6h6V24h-8v-4c0-7.73 6.27-14
-14-14s14 6.27 14 14v4h-8v16h6c3.31 0 6-2.69 6-6V20c0-9.94-8.06-18-18-18z" />
-</vector>
diff --git a/res/drawable/music_action_background.xml b/res/drawable/ic_queue_button.xml
index 00d61c6..389c266 100644
--- a/res/drawable/music_action_background.xml
+++ b/res/drawable/ic_queue_button.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright 2016, The Android Open Source Project
+ Copyright 2019, The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,7 +14,10 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<inset xmlns:android="http://schemas.android.com/apk/res/android"
- android:inset="@dimen/music_action_ripple_inset" >
- <ripple android:color="@color/car_card_ripple_background" />
-</inset>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/selectable_button_background"
+ android:height="@dimen/queue_button_background_size"
+ android:width="@dimen/queue_button_background_size"/>
+ <item android:drawable="@drawable/ic_tracklist"
+ android:gravity="center"/>
+</layer-list>
diff --git a/res/drawable/ic_search.xml b/res/drawable/ic_search.xml
new file mode 100644
index 0000000..e6056f3
--- /dev/null
+++ b/res/drawable/ic_search.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2018 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
+</vector>
diff --git a/res/drawable/ic_settings.xml b/res/drawable/ic_settings.xml
new file mode 100644
index 0000000..6fae5ea
--- /dev/null
+++ b/res/drawable/ic_settings.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2018 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M21.4 14.2l-1.94-1.45c.03-.25 .04 -.5 .04 -.76s-.01-.51-.04-.76L21.4 9.8c.42-.31
+.52 -.94 .24 -1.41l-1.6-2.76c-.28-.48-.88-.7-1.36-.5l-2.14 .91
+c-.48-.37-1.01-.68-1.57-.92l-.27-2.2c-.06-.52-.56-.92-1.11-.92h-3.18c-.55 0-1.05
+.4 -1.11 .92 l-.26 2.19c-.57 .24 -1.1 .55 -1.58 .92 l-2.14-.91c-.48-.2-1.08 .02
+-1.36 .5 l-1.6 2.76c-.28 .48 -.18 1.1 .24 1.42l1.94 1.45c-.03 .24 -.04 .49 -.04
+.75 s.01 .51 .04 .76 L2.6 14.2c-.42 .31 -.52 .94 -.24 1.41l1.6 2.76c.28 .48 .88
+.7 1.36 .5 l2.14-.91c.48 .37 1.01 .68 1.57 .92 l.27 2.19c.06 .53 .56 .93 1.11
+.93 h3.18c.55 0 1.04-.4 1.11-.92l.27-2.19c.56-.24 1.09-.55 1.57-.92l2.14 .91
+c.48 .2 1.08-.02 1.36-.5l1.6-2.76c.28-.48 .18 -1.1-.24-1.42zM12 15.5c-1.93
+0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"/>
+</vector>
diff --git a/res/drawable/ic_tracklist.xml b/res/drawable/ic_tracklist.xml
deleted file mode 100644
index ac1522b..0000000
--- a/res/drawable/ic_tracklist.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2018, The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="56dp"
- android:height="56dp"
- android:viewportWidth="48"
- android:viewportHeight="48">
-
- <path
- android:pathData="M0 0h48v48H0z" />
- <path
- android:fillColor="#000000"
- android:pathData="M30 12H6v4h24v-4zm0 8H6v4h24v-4zM6
-32h16v-4H6v4zm28-20v16.37c-.63-.23-1.29-.37-2-.37-3.31 0-6 2.69-6 6s2.69 6 6 6
-6-2.69 6-6V16h6v-4H34z" />
-</vector>
diff --git a/res/drawable/media_app_title_background.xml b/res/drawable/media_app_title_background.xml
new file mode 100644
index 0000000..738ec50
--- /dev/null
+++ b/res/drawable/media_app_title_background.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<shape/> \ No newline at end of file
diff --git a/res/drawable/seekbar_background.xml b/res/drawable/seekbar_background.xml
index b66f6ee..ec08455 100644
--- a/res/drawable/seekbar_background.xml
+++ b/res/drawable/seekbar_background.xml
@@ -16,11 +16,18 @@
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:id="@android:id/background"
- android:drawable="@color/progress_bar_background" />
+ <item android:id="@android:id/background">
+ <shape android:shape="line">
+ <stroke android:width="@dimen/playback_seekbar_track_height"/>
+ </shape>
+ </item>
<item android:id="@android:id/progress">
- <clip android:drawable="@color/progress_bar_highlight" />
+ <clip>
+ <shape android:shape="line">
+ <stroke android:width="@dimen/playback_seekbar_track_height"/>
+ </shape>
+ </clip>
</item>
</layer-list>
diff --git a/res/drawable/seekbar_thumb.xml b/res/drawable/seekbar_thumb.xml
index d78f200..1d0b4a1 100644
--- a/res/drawable/seekbar_thumb.xml
+++ b/res/drawable/seekbar_thumb.xml
@@ -15,10 +15,9 @@
limitations under the License.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
- android:shape="rectangle">
- <!-- Transparent thumb, used to leave a gap between the two segments of the progress bar -->
- <solid android:color="@android:color/transparent"/>
+ android:shape="oval">
+ <solid android:color="@color/progress_bar_thumb_color"/>
<size
android:width="@dimen/playback_seekbar_thumb_width"
- android:height="@dimen/playback_seekbar_height"/>
+ android:height="@dimen/playback_seekbar_thumb_height"/>
</shape> \ No newline at end of file
diff --git a/res/layout-h1200dp/fragment_metadata.xml b/res/layout-h1200dp/fragment_metadata.xml
deleted file mode 100644
index 63412aa..0000000
--- a/res/layout-h1200dp/fragment_metadata.xml
+++ /dev/null
@@ -1,65 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2018, The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<merge
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- tools:showIn="@layout/fragment_playback">
-
- <ImageView
- android:id="@+id/album_art"
- android:layout_width="@dimen/playback_album_art_size_large"
- android:layout_height="@dimen/playback_album_art_size_large"
- android:layout_marginEnd="@dimen/car_keyline_2"
- android:layout_marginStart="@dimen/car_keyline_2"
- android:contentDescription="@string/album_art"
- android:background="@color/car_body1_light"
- android:scaleType="centerCrop"
- android:transitionName="@string/album_art"
- app:layout_constraintBottom_toTopOf="@id/metadata_subcontainer"
- app:layout_constraintEnd_toEndOf="@+id/margin_end"
- app:layout_constraintStart_toStartOf="@+id/margin_start"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintVertical_chainStyle="packed"
- tools:src="@drawable/ic_person"/>
-
- <include
- android:id="@+id/metadata_subcontainer"
- layout="@layout/metadata_normal"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginTop="@dimen/car_padding_5"
- android:layout_marginBottom="@dimen/playback_controls_margin"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="@+id/album_art"
- app:layout_constraintStart_toStartOf="@+id/album_art"
- app:layout_constraintTop_toBottomOf="@+id/album_art"/>
-
- <androidx.car.widget.PagedListView
- android:id="@+id/queue_list"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_marginTop="@dimen/playback_album_art_size_normal"
- android:layout_marginBottom="@dimen/playback_controls_margin"
- android:visibility="gone"
- app:dividerEndMargin="@dimen/car_keyline_1"
- app:dividerStartMargin="@dimen/car_keyline_1"
- app:layout_behavior="@string/appbar_scrolling_view_behavior"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/margin_top"
- app:listDividerColor="@color/car_list_divider_light"/>
-</merge>
diff --git a/res/layout-h1200dp/fragment_metadata_with_queue.xml b/res/layout-h1200dp/fragment_metadata_with_queue.xml
deleted file mode 100644
index 26bfb26..0000000
--- a/res/layout-h1200dp/fragment_metadata_with_queue.xml
+++ /dev/null
@@ -1,62 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2018, The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<merge
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- tools:showIn="@layout/fragment_playback_with_queue">
-
-
- <ImageView
- android:id="@+id/album_art"
- android:layout_width="@dimen/playback_album_art_size_normal"
- android:layout_height="@dimen/playback_album_art_size_normal"
- android:layout_marginTop="@dimen/car_padding_4"
- android:layout_marginStart="@dimen/car_keyline_1"
- android:contentDescription="@string/album_art"
- android:background="@color/car_body1_light"
- android:scaleType="centerCrop"
- android:transitionName="@string/album_art"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintStart_toStartOf="@+id/margin_start"
- tools:src="@drawable/ic_person"/>
-
- <include android:id="@+id/metadata_subcontainer"
- layout="@layout/metadata_normal"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/car_padding_4"
- android:layout_marginEnd="@dimen/car_keyline_1"
- app:layout_constraintBottom_toBottomOf="@+id/album_art"
- app:layout_constraintEnd_toEndOf="@+id/margin_end"
- app:layout_constraintStart_toEndOf="@+id/album_art"
- app:layout_constraintTop_toTopOf="@+id/album_art"/>
-
- <androidx.car.widget.PagedListView
- android:id="@+id/queue_list"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_marginTop="@dimen/car_padding_4"
- android:layout_marginBottom="@dimen/playback_controls_margin"
- app:dividerEndMargin="@dimen/car_keyline_1"
- app:dividerStartMargin="@dimen/car_keyline_1"
- app:layout_behavior="@string/appbar_scrolling_view_behavior"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/album_art"
- app:listDividerColor="@color/car_list_divider_light"/>
-
-</merge>
diff --git a/res/layout-port/metadata_normal.xml b/res/layout-port/metadata_normal.xml
new file mode 100644
index 0000000..3e579e6
--- /dev/null
+++ b/res/layout-port/metadata_normal.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2019, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <ImageView
+ android:id="@+id/album_art"
+ android:layout_width="@dimen/playback_album_art_size"
+ android:layout_height="@dimen/playback_album_art_size"
+ android:contentDescription="@string/album_art"
+ android:background="@color/album_art_background"
+ android:scaleType="centerCrop"
+ android:transitionName="@string/album_art"
+ app:layout_constraintVertical_chainStyle="packed"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@+id/title"/>
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:layout_marginTop="@dimen/art_metadata_margin"
+ style="@style/MetadataPlaybackTitleStyle"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/album_art"
+ app:layout_constraintBottom_toTopOf="@+id/album_title"
+ app:layout_constraintVertical_chainStyle="packed"/>
+
+ <TextView
+ android:id="@+id/artist"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:layout_marginTop="@dimen/metadata_title_subtitle_margin"
+ style="@style/MetadataPlaybackSubtitleStyle"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/title"
+ app:layout_constraintBottom_toTopOf="@+id/album_title"/>
+
+
+ <TextView
+ android:id="@+id/album_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/metadata_subtitles_margin"
+ style="@style/MetadataPlaybackSubtitleStyle"
+ app:layout_goneMarginTop="@dimen/metadata_title_subtitle_margin"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/progress_text_container"
+ app:layout_constraintTop_toBottomOf="@id/artist"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constrainedWidth="true"/>
+
+ <include
+ android:id="@+id/progress_text_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintStart_toEndOf="@+id/album_title"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="@+id/album_title"
+ app:layout_constraintBottom_toBottomOf="@+id/album_title"
+ layout="@layout/time_progress_text"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/app_selection_item.xml b/res/layout/app_selection_item.xml
deleted file mode 100644
index 69ab513..0000000
--- a/res/layout/app_selection_item.xml
+++ /dev/null
@@ -1,48 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~
- ~ Copyright (C) 2018 Google Inc.
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- ~
- -->
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/app_item"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/car_padding_2"
- android:layout_marginEnd="@dimen/car_padding_2"
- android:layout_marginBottom="@dimen/car_padding_5"
- android:orientation="vertical"
- android:background="@drawable/app_item_background"
- android:padding="@dimen/car_padding_1"
- android:gravity="center">
-
- <ImageView
- android:id="@+id/app_icon"
- android:layout_width="@dimen/car_touch_target_size"
- android:layout_height="@dimen/car_touch_target_size"
- android:layout_marginBottom="@dimen/car_padding_4"
- android:layout_gravity="center_horizontal" />
-
- <TextView
- android:id="@+id/app_name"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:textAppearance="@style/TextAppearance.Car.Label1"
- android:textColor="@color/car_label1_light"
- android:gravity="center"
- android:ellipsize="end"
- android:maxLines="1" />
-</LinearLayout>
diff --git a/res/layout/appbar_view.xml b/res/layout/appbar_view.xml
index e928f37..232db86 100644
--- a/res/layout/appbar_view.xml
+++ b/res/layout/appbar_view.xml
@@ -15,63 +15,97 @@
limitations under the License.
-->
<merge
- xmlns:android="http://schemas.android.com/apk/res/android">
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/row_separator"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:orientation="horizontal"
+ app:layout_constraintGuide_begin="@dimen/appbar_first_row_height"/>
<FrameLayout
android:id="@+id/nav_icon_container"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentLeft="true"
- android:layout_centerInParent="true"
- android:layout_marginLeft="34dp"
- android:layout_toLeftOf="@+id/app_switch_indicator"
- android:padding="@dimen/car_padding_1"
- android:background="@drawable/app_item_background">
+ android:layout_width="@dimen/appbar_view_nav_button_width"
+ android:layout_height="@dimen/appbar_first_row_height"
+ android:padding="@dimen/app_switch_widget_icon_padding"
+ android:background="@drawable/appbar_view_icon_background"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/row_separator"
+ app:layout_constraintStart_toStartOf="parent">
<ImageView
android:id="@+id/nav_icon"
- android:layout_width="@dimen/car_primary_icon_size"
- android:layout_height="@dimen/car_primary_icon_size"
- android:tint="@color/car_body1_light"/>
+ android:layout_width="@dimen/appbar_view_icon_size"
+ android:layout_height="@dimen/appbar_view_icon_size"
+ android:layout_gravity="center"
+ android:scaleType="fitXY"
+ android:tint="@color/appbar_view_icon_tint"/>
</FrameLayout>
- <LinearLayout
+ <com.android.car.apps.common.widget.CarTabLayout
android:id="@+id/tabs"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:orientation="horizontal"
- android:gravity="center"
- android:layout_alignParentLeft="true"
- android:layout_centerInParent="true"/>
+ style="@style/AppBarTabStyle"/>
- <LinearLayout
- android:id="@+id/app_switch_container"
- android:layout_width="@dimen/car_margin"
- android:layout_height="wrap_content"
- android:layout_alignParentRight="true"
- android:layout_centerInParent="true"
- android:padding="@dimen/car_padding_1"
- android:orientation="horizontal"
- android:background="@drawable/app_item_background"
- android:gravity="center">
+ <com.android.car.media.widgets.SearchBar
+ android:id="@+id/search_bar_container"
+ android:layout_height="0dp"
+ android:layout_width="0dp"
+ android:layout_marginStart="@dimen/appbar_view_nav_button_width"
+ android:layout_marginEnd="@dimen/app_switch_widget_width"
+ android:visibility="gone"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/row_separator"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent">
+ </com.android.car.media.widgets.SearchBar>
- <ImageView
- android:id="@+id/app_icon"
- android:layout_width="@dimen/car_primary_icon_size"
- android:layout_height="@dimen/car_primary_icon_size"/>
+ <ImageView
+ android:id="@+id/search"
+ android:layout_width="@dimen/appbar_view_icon_touch_target_size"
+ android:layout_height="@dimen/appbar_view_icon_touch_target_size"
+ android:layout_marginEnd="@dimen/appbar_view_control_buttons_spacing"
+ android:padding="@dimen/appbar_view_icon_padding"
+ android:tint="@color/appbar_view_icon_tint"
+ android:src="@drawable/ic_search"
+ android:background="@drawable/appbar_view_icon_background"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/row_separator"
+ app:layout_constraintEnd_toStartOf="@+id/settings"
+ app:layout_goneMarginEnd="@dimen/appbar_view_control_buttons_margin_end"/>
- <ImageView
- android:id="@+id/app_switch_icon"
- android:layout_width="@dimen/car_primary_icon_size"
- android:layout_height="@dimen/car_primary_icon_size"
- android:tint="@color/car_body1_light"/>
+ <com.android.car.apps.common.UxrButton
+ android:id="@+id/settings"
+ android:layout_width="@dimen/appbar_view_icon_touch_target_size"
+ android:layout_height="@dimen/appbar_view_icon_touch_target_size"
+ android:layout_marginEnd="@dimen/appbar_view_control_buttons_margin_end"
+ android:padding="@dimen/appbar_view_icon_padding"
+ android:drawableTint="@color/appbar_view_settings_tint"
+ android:drawableBottom="@drawable/ic_settings"
+ android:background="@drawable/appbar_view_icon_background"
+ app:carUxRestrictions="UX_RESTRICTIONS_NO_SETUP"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/row_separator"
+ app:layout_constraintEnd_toStartOf="@+id/app_switch_container"/>
- </LinearLayout>
+ <com.android.car.media.common.MediaAppSelectorWidget
+ android:id="@+id/app_switch_container"
+ android:layout_width="@dimen/app_switch_widget_width"
+ android:layout_height="@dimen/appbar_view_icon_touch_target_size"
+ android:orientation="horizontal"
+ android:gravity="center"
+ android:background="@drawable/appbar_view_icon_background"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/row_separator"
+ app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerInParent="true"
- android:textAppearance="@style/TextAppearance.Car.Body1.Light"/>
+ style="@style/AppBarTitleStyle"
+ android:layout_marginStart="@dimen/appbar_view_title_margin_start"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/row_separator"
+ app:layout_constraintStart_toEndOf="@id/nav_icon_container"
+ app:layout_constraintEnd_toStartOf="@id/search"/>
-</merge> \ No newline at end of file
+</merge>
diff --git a/res/layout/browse_state.xml b/res/layout/browse_state.xml
index f85b1a2..32ab839 100644
--- a/res/layout/browse_state.xml
+++ b/res/layout/browse_state.xml
@@ -14,38 +14,39 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<merge
+<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
tools:showIn="@layout/fragment_browse">
- <ProgressBar
- android:id="@+id/loading_spinner"
- android:layout_width="match_parent"
- android:layout_height="@dimen/car_double_line_list_item_height"
- android:layout_centerInParent="true"
- android:visibility="gone"
- android:indeterminateDrawable="@drawable/music_buffering" />
-
<ImageView
android:id="@+id/error_icon"
android:layout_width="@dimen/missing_permission_icon_size"
android:layout_height="@dimen/missing_permission_icon_size"
- android:layout_centerInParent="true"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@+id/error_message"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintVertical_chainStyle="packed"
android:src="@drawable/error_illustration"
android:visibility="gone"
- android:tint="@color/car_body2_light"/>
+ android:tint="@color/icon_tint"/>
<TextView
android:id="@+id/error_message"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
+ style="@style/ErrorTextStyle"
android:layout_height="wrap_content"
- android:layout_below="@+id/error_icon"
- android:layout_marginTop="@dimen/car_padding_4"
- android:textAppearance="@style/TextAppearance.Car.Body2.Light"
- android:gravity="center"
+ android:layout_marginTop="@dimen/browse_state_error_margin_top"
+ app:layout_constraintTop_toBottomOf="@+id/error_icon"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
android:maxLines="3"
android:text="@string/nothing_to_play"
android:visibility="gone" />
-</merge> \ No newline at end of file
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/res/layout/fragment_app_selection.xml b/res/layout/fragment_app_selection.xml
deleted file mode 100644
index 431f394..0000000
--- a/res/layout/fragment_app_selection.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~
- ~ Copyright (C) 2018 Google Inc.
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- ~
- -->
-<androidx.constraintlayout.widget.ConstraintLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:background="@color/alpha_fragment_background_color"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <androidx.car.widget.PagedListView
- android:id="@+id/apps_grid"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_marginTop="@dimen/appbar_height"
- android:clickable="true"
- android:focusable="true"
- app:listContentTopOffset="@dimen/car_padding_5"
- app:dayNightStyle="always_light"
- app:showPagedListViewDivider="false" />
-
-</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/fragment_browse.xml b/res/layout/fragment_browse.xml
index 60bac4d..bac159a 100644
--- a/res/layout/fragment_browse.xml
+++ b/res/layout/fragment_browse.xml
@@ -16,21 +16,19 @@
-->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/browse_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/browse_state"/>
- <androidx.car.widget.PagedListView
+ <com.android.car.apps.common.widget.PagedRecyclerView
android:id="@+id/browse_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:focusable="true"
- android:visibility="gone"
- app:dayNightStyle="always_light"
- app:listDividerColor="@color/car_list_divider_light"/>
+ android:clipToPadding="false"
+ android:visibility="gone" />
</RelativeLayout>
diff --git a/res/layout/fragment_empty.xml b/res/layout/fragment_empty.xml
index 7f53519..43fd5fe 100644
--- a/res/layout/fragment_empty.xml
+++ b/res/layout/fragment_empty.xml
@@ -21,5 +21,4 @@
android:layout_height="match_parent">
<include layout="@layout/browse_state"/>
-
-</RelativeLayout> \ No newline at end of file
+</RelativeLayout>
diff --git a/res/layout-h1200dp/fragment_playback_with_queue.xml b/res/layout/fragment_error.xml
index 1fec4fd..db0935d 100644
--- a/res/layout-h1200dp/fragment_playback_with_queue.xml
+++ b/res/layout/fragment_error.xml
@@ -14,38 +14,31 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<androidx.constraintlayout.widget.ConstraintLayout
+<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/playback_container"
+ android:id="@+id/error_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent">
- <androidx.constraintlayout.widget.Guideline
- android:id="@+id/margin_start"
- android:layout_width="0dp"
- android:layout_height="0dp"
- android:orientation="vertical"
- app:layout_constraintGuide_begin="@dimen/car_margin"/>
-
- <androidx.constraintlayout.widget.Guideline
- android:id="@+id/margin_end"
- android:layout_width="wrap_content"
+ <com.android.car.apps.common.UxrTextView
+ android:id="@+id/error_message"
+ style="@style/ErrorTextStyle"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:orientation="vertical"
- app:layout_constraintGuide_end="@dimen/car_margin"/>
-
- <androidx.constraintlayout.widget.Guideline
- android:id="@+id/margin_top"
+ android:gravity="center"
+ android:layout_marginHorizontal="@dimen/fragment_error_message_margin_side"
+ android:layout_marginTop="@dimen/fragment_error_margin_top"/>
+
+ <com.android.car.apps.common.UxrButton
+ android:id="@+id/error_button"
+ style="@style/ErrorButtonStyle"
+ android:textColor="@color/error_button_text_color"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:orientation="horizontal"
- app:layout_constraintGuide_begin="@dimen/car_padding_4"/>
-
- <include
- layout="@layout/fragment_metadata_with_queue"/>
-
- <include
- layout="@layout/playback_controls"/>
+ android:layout_marginTop="@dimen/fragment_error_button_margin_top"
+ android:layout_centerHorizontal="true"
+ android:layout_below="@+id/error_message"
+ app:carUxRestrictions="UX_RESTRICTIONS_NO_SETUP"/>
-</androidx.constraintlayout.widget.ConstraintLayout>
+</RelativeLayout>
diff --git a/res/layout/fragment_metadata.xml b/res/layout/fragment_metadata.xml
deleted file mode 100644
index 7e0bf7d..0000000
--- a/res/layout/fragment_metadata.xml
+++ /dev/null
@@ -1,64 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2018, The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<merge
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- tools:showIn="@layout/fragment_playback_with_queue">
-
-
- <ImageView
- android:id="@+id/album_art"
- android:layout_width="@dimen/playback_album_art_size_normal"
- android:layout_height="@dimen/playback_album_art_size_normal"
- android:layout_marginStart="@dimen/car_keyline_2"
- android:contentDescription="@string/album_art"
- android:background="@color/car_body1_light"
- android:scaleType="centerCrop"
- android:transitionName="@string/album_art"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toTopOf="@+id/playback_controls"
- app:layout_constraintStart_toStartOf="@+id/margin_start"
- tools:src="@drawable/ic_person"/>
-
- <include
- android:id="@+id/metadata_subcontainer"
- layout="@layout/metadata_normal"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/car_padding_5"
- android:layout_marginEnd="@dimen/car_keyline_2"
- app:layout_constraintBottom_toBottomOf="@+id/album_art"
- app:layout_constraintEnd_toEndOf="@+id/margin_end"
- app:layout_constraintStart_toEndOf="@+id/album_art"
- app:layout_constraintTop_toTopOf="@+id/album_art"/>
-
- <androidx.car.widget.PagedListView
- android:id="@+id/queue_list"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_marginTop="@dimen/car_padding_4"
- android:layout_marginBottom="@dimen/playback_controls_margin"
- android:visibility="gone"
- app:dividerEndMargin="@dimen/car_keyline_1"
- app:dividerStartMargin="@dimen/car_keyline_1"
- app:layout_behavior="@string/appbar_scrolling_view_behavior"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/album_art"
- app:listDividerColor="@color/car_list_divider_light"/>
-
-</merge>
diff --git a/res/layout/fragment_playback.xml b/res/layout/fragment_playback.xml
index b08bf8d..982e00a 100644
--- a/res/layout/fragment_playback.xml
+++ b/res/layout/fragment_playback.xml
@@ -22,30 +22,158 @@
android:layout_height="match_parent">
<androidx.constraintlayout.widget.Guideline
- android:id="@+id/margin_start"
+ android:id="@+id/app_bar_guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:layout_constraintGuide_begin="@dimen/appbar_first_row_height"/>
+
+ <Space
+ android:id="@+id/control_bar_first_row_guideline"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/control_bar_height"
+ android:layout_marginBottom="@dimen/control_bar_margin_bottom"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+
+ <com.android.car.apps.common.BackgroundImageView
+ android:id="@+id/playback_background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <include
+ layout="@layout/scrim_overlay"
+ android:id="@+id/background_scrim"
android:layout_width="0dp"
android:layout_height="0dp"
- android:orientation="vertical"
- app:layout_constraintGuide_begin="@dimen/car_margin"/>
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"/>
- <androidx.constraintlayout.widget.Guideline
- android:id="@+id/margin_end"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:orientation="vertical"
- app:layout_constraintGuide_end="@dimen/car_margin"/>
+ <FrameLayout
+ android:id="@+id/nav_icon_container"
+ android:layout_width="@dimen/appbar_view_nav_button_width"
+ android:layout_height="@dimen/appbar_first_row_height"
+ android:padding="@dimen/app_switch_widget_icon_padding"
+ android:background="@drawable/appbar_view_icon_background"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent">
+ <ImageView
+ android:id="@+id/nav_icon"
+ android:layout_width="@dimen/appbar_view_icon_size"
+ android:layout_height="@dimen/appbar_view_icon_size"
+ android:src="@drawable/ic_expand_more"
+ android:layout_gravity="center"
+ android:scaleType="fitXY"
+ android:tint="@color/appbar_view_icon_tint"/>
+ </FrameLayout>
- <androidx.constraintlayout.widget.Guideline
- android:id="@+id/margin_top"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
+ <TextView
+ android:id="@+id/playback_title"
+ style="@style/AppBarTitleStyle"
+ android:text="@string/fragment_playback_title"
+ app:layout_constraintTop_toTopOf="@id/nav_icon_container"
+ app:layout_constraintBottom_toBottomOf="@id/nav_icon_container"
+ app:layout_constraintLeft_toRightOf="@id/nav_icon_container"
+ app:layout_constraintRight_toLeftOf="@+id/queue_button"/>
+
+ <ImageView
+ android:id="@+id/queue_button"
+ android:layout_width="@dimen/appbar_view_icon_touch_target_size"
+ android:layout_height="@dimen/appbar_view_icon_touch_target_size"
+ android:layout_marginEnd="@dimen/playback_queue_button_margin_end"
+ android:src="@drawable/ic_queue_button"
+ android:scaleType="center"
+ android:background="@drawable/appbar_view_icon_background"
+ app:layout_constraintTop_toTopOf="@id/nav_icon_container"
+ app:layout_constraintBottom_toBottomOf="@id/nav_icon_container"
+ app:layout_constraintRight_toLeftOf="@+id/app_icon_container"/>
+
+ <com.android.car.media.common.MediaAppSelectorWidget
+ android:id="@+id/app_icon_container"
+ android:layout_width="@dimen/app_switch_widget_width"
+ android:layout_height="@dimen/appbar_view_icon_touch_target_size"
+ android:padding="@dimen/app_switch_widget_icon_padding"
android:orientation="horizontal"
- app:layout_constraintGuide_begin="@dimen/car_padding_4"/>
+ android:gravity="center"
+ android:background="@drawable/appbar_view_icon_background"
+ app:switchingEnabled="false"
+ app:layout_constraintTop_toTopOf="@id/nav_icon_container"
+ app:layout_constraintBottom_toBottomOf="@id/nav_icon_container"
+ app:layout_constraintRight_toRightOf="parent"/>
<include
- layout="@layout/fragment_metadata"/>
+ style="@style/MetadataContainerStyle"
+ android:id="@+id/metadata_container"
+ layout="@layout/metadata_normal"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/app_bar_guideline"
+ app:layout_constraintBottom_toTopOf="@+id/control_bar_first_row_guideline"/>
+
+ <SeekBar
+ style="@style/SeekBarStyle"
+ android:id="@+id/seek_bar"
+ android:layout_gravity="center"
+ android:clickable="false"
+ android:focusable="false"
+ android:layout_marginStart="@dimen/playback_seekbar_margin_x"
+ android:layout_marginEnd="@dimen/playback_seekbar_margin_x"
+ android:paddingEnd="@dimen/playback_seekbar_padding_x"
+ android:paddingStart="@dimen/playback_seekbar_padding_x"
+ android:progressDrawable="@drawable/seekbar_background"
+ android:thumb="@drawable/seekbar_thumb"
+ android:thumbOffset="@dimen/playback_seekbar_thumb_offset"
+ android:splitTrack="false"
+ android:progressTint="@color/progress_bar_highlight"
+ android:progressBackgroundTint="@color/progress_bar_background"
+ android:background="@null"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/metadata_container"
+ app:layout_constraintBottom_toTopOf="@+id/control_bar_first_row_guideline"/>
+
+ <Space
+ android:id="@+id/queue_list_top_constraint"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/fragment_playback_queue_overlap_top"
+ app:layout_constraintBottom_toBottomOf="@+id/app_bar_guideline"/>
+
+ <Space
+ android:id="@+id/queue_list_bottom_constraint"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/fragment_playback_queue_overlap_bottom"
+ app:layout_constraintTop_toTopOf="@+id/control_bar_first_row_guideline"/>
+
+ <com.android.car.apps.common.widget.PagedRecyclerView
+ android:id="@+id/queue_list"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:visibility="gone"
+ app:layout_constraintTop_toTopOf="@+id/queue_list_top_constraint"
+ app:layout_constraintBottom_toBottomOf="@+id/queue_list_bottom_constraint"
+ android:fadeScrollbars="true"
+ android:requiresFadingEdge="vertical"
+ android:fadingEdgeLength="@dimen/queue_fading_edge_length"/>
<include
- layout="@layout/playback_controls"/>
+ layout="@layout/scrim_overlay"
+ android:id="@+id/control_bar_scrim"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"/>
+ <com.android.car.media.common.PlaybackControlsActionBar
+ android:id="@+id/playback_controls"
+ style="@style/ControlBar"
+ android:layout_marginHorizontal="@dimen/control_bar_margin_x"
+ android:layout_marginBottom="@dimen/control_bar_margin_bottom"
+ app:columns="5"
+ app:enableOverflow="true"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/media_browse_more_footer.xml b/res/layout/fragment_search.xml
index 0cb3304..fb5e0fd 100644
--- a/res/layout/media_browse_more_footer.xml
+++ b/res/layout/fragment_search.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright 2018, The Android Open Source Project
+ Copyright 2019, The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,19 +14,23 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<FrameLayout
+<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/container"
+ android:id="@+id/browse_container"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/car_keyline_1"
- android:layout_marginEnd="@dimen/car_keyline_1">
- <TextView
- android:id="@+id/title"
- android:text="@string/media_browse_more"
+ android:layout_height="match_parent">
+
+ <include layout="@layout/browse_state"/>
+
+ <com.android.car.apps.common.widget.PagedRecyclerView
+ android:id="@+id/browse_list"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginBottom="@dimen/car_padding_1"
- android:includeFontPadding="false"
- android:textAppearance="@style/TextAppearance.Car.Body1.Light"/>
-</FrameLayout>
+ android:layout_height="match_parent"
+ android:clickable="true"
+ android:focusable="true"
+ android:paddingTop="@dimen/search_fragment_top_padding"
+ android:paddingBottom="@dimen/search_fragment_bottom_padding"
+ android:clipToPadding="false"
+ android:visibility="gone" />
+
+</RelativeLayout>
diff --git a/res/layout/media_activity.xml b/res/layout/media_activity.xml
index 68f912c..6ec79c9 100644
--- a/res/layout/media_activity.xml
+++ b/res/layout/media_activity.xml
@@ -17,75 +17,63 @@
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/application"
+ android:id="@+id/media_activity_root"
android:layout_width="match_parent"
android:layout_height="match_parent">
- <com.android.car.media.common.CrossfadeImageView
- android:id="@+id/media_background"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@android:color/black"
- android:scaleType="centerCrop"/>
-
- <View
- android:id="@+id/media_scrim"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@color/album_art_scrim"
- android:alpha="@dimen/album_art_scrim_alpha"/>
-
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="visible"
- app:layout_constraintTop_toBottomOf="@+id/app_bar"
- app:layout_constraintBottom_toTopOf="@+id/browse_controls_container"/>
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"/>
<FrameLayout
- android:id="@+id/playback_container"
+ android:id="@+id/search_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="gone"
- app:layout_constraintTop_toBottomOf="@+id/app_bar"
+ app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
- <LinearLayout
- android:id="@+id/browse_controls_container"
+ <FrameLayout
+ android:id="@+id/error_container"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:padding="@dimen/car_padding_4"
- android:background="@color/browse_playback_bg"
- android:orientation="horizontal"
- android:gravity="center_vertical"
+ android:layout_height="0dp"
+ android:visibility="gone"
+ app:layout_constraintTop_toBottomOf="@+id/app_bar"
app:layout_constraintBottom_toBottomOf="parent">
-
- <com.android.car.media.common.PlaybackControlsActionBar
- android:id="@+id/browse_controls"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="0.5"
- android:layout_marginRight="@dimen/car_padding_4"
- app:columns="3"/>
-
- <com.android.car.media.widgets.MetadataView
- android:id="@+id/browse_metadata"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="0.5"/>
-
- </LinearLayout>
+ </FrameLayout>
<FrameLayout
android:id="@+id/app_selection_container"
android:layout_width="match_parent"
- android:layout_height="match_parent" />
+ android:layout_height="match_parent"/>
<com.android.car.media.widgets.AppBarView
android:id="@+id/app_bar"
- android:layout_height="@dimen/car_app_bar_height"
android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/app_bar_background_color"
app:layout_constraintTop_toTopOf="parent"/>
+ <com.android.car.media.common.MinimizedPlaybackControlBar
+ android:id="@+id/minimized_playback_controls"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/minimized_control_bar_margin_x"
+ android:layout_marginBottom="@dimen/minimized_control_bar_margin_bottom"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+
+ <FrameLayout
+ android:id="@+id/playback_container"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:visibility="gone"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/media_browse_grid_item.xml b/res/layout/media_browse_grid_item.xml
index 00dbbb8..4f4450e 100644
--- a/res/layout/media_browse_grid_item.xml
+++ b/res/layout/media_browse_grid_item.xml
@@ -14,31 +14,92 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<LinearLayout
+<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/media_browse_grid_item_padding"
android:focusable="true"
android:clickable="true"
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
+ android:foreground="@drawable/grid_item_background"
+ android:layout_marginBottom="@dimen/media_browse_grid_item_margin_bottom">
<com.android.car.media.common.FixedRatioImageView
android:id="@+id/thumbnail"
android:layout_width="match_parent"
android:layout_height="0dp"
android:scaleType="centerCrop"
- app:aspect_ratio="1"/>
+ app:aspect_ratio="1"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+ <ImageView
+ android:id="@+id/download_icon_with_title"
+ android:layout_width="@dimen/media_browse_indicator_size"
+ android:layout_height="@dimen/media_browse_indicator_size"
+ android:src="@drawable/ic_file_download_done_black"
+ android:tint="@color/icon_tint"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/explicit_icon_with_title"
+ app:layout_constraintBottom_toBottomOf="@+id/title"
+ app:layout_constraintTop_toTopOf="@+id/title"/>
+
+ <ImageView
+ android:id="@+id/explicit_icon_with_title"
+ android:layout_width="@dimen/media_browse_indicator_size"
+ android:layout_height="@dimen/media_browse_indicator_size"
+ android:src="@drawable/ic_explicit_black"
+ android:tint="@color/icon_tint"
+ app:layout_constraintBottom_toBottomOf="@+id/title"
+ app:layout_constraintTop_toTopOf="@+id/title"
+ app:layout_constraintStart_toEndOf="@+id/download_icon_with_title"
+ app:layout_constraintEnd_toStartOf="@+id/title"/>
<TextView
android:id="@+id/title"
- android:layout_width="match_parent"
+ style="@style/BrowseGridTitleStyle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/media_browse_grid_item_text_margin_top"
+ android:singleLine="true"
+ android:duplicateParentState="true"
+ app:layout_constraintStart_toEndOf="@+id/explicit_icon_with_title"
+ app:layout_constraintTop_toBottomOf="@+id/thumbnail"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+ <ImageView
+ android:id="@+id/download_icon_with_subtitle"
+ android:layout_width="@dimen/media_browse_indicator_size"
+ android:layout_height="@dimen/media_browse_indicator_size"
+ android:src="@drawable/ic_file_download_done_black"
+ android:tint="@color/icon_tint"
+ app:layout_constraintBottom_toBottomOf="@+id/subtitle"
+ app:layout_constraintTop_toTopOf="@+id/subtitle"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/explicit_icon_with_subtitle"/>
+
+ <ImageView
+ android:id="@+id/explicit_icon_with_subtitle"
+ android:layout_width="@dimen/media_browse_indicator_size"
+ android:layout_height="@dimen/media_browse_indicator_size"
+ android:src="@drawable/ic_explicit_black"
+ android:tint="@color/icon_tint"
+ app:layout_constraintBottom_toBottomOf="@+id/subtitle"
+ app:layout_constraintTop_toTopOf="@+id/subtitle"
+ app:layout_constraintStart_toEndOf="@+id/download_icon_with_subtitle"/>
+
+ <TextView
+ android:id="@+id/subtitle"
+ style="@style/BrowseGridSubtitleStyle"
+ android:layout_width="0dp"
android:layout_height="wrap_content"
- android:layout_marginTop="@dimen/car_padding_2"
- android:layout_marginBottom="@dimen/car_padding_2"
- android:ellipsize="end"
- android:maxLines="1"
- android:textAppearance="@style/TextAppearance.Car.Body2.Light"/>
+ android:singleLine="true"
+ android:duplicateParentState="true"
+ app:layout_constraintTop_toBottomOf="@+id/title"
+ app:layout_constraintStart_toEndOf="@+id/explicit_icon_with_subtitle"
+ app:layout_constraintEnd_toEndOf="parent"/>
-</LinearLayout> \ No newline at end of file
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/res/layout/media_browse_header_item.xml b/res/layout/media_browse_header_item.xml
index 89d38ed..3e17f98 100644
--- a/res/layout/media_browse_header_item.xml
+++ b/res/layout/media_browse_header_item.xml
@@ -18,13 +18,13 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/car_keyline_1"
- android:layout_marginEnd="@dimen/car_keyline_1">
+ android:layout_height="@dimen/media_browse_header_item_height"
+ android:layout_marginHorizontal="@dimen/media_browse_header_item_margin_x">
<TextView
android:id="@+id/title"
+ style="@style/BrowseSubheaderStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:includeFontPadding="false"
- android:textAppearance="@style/TextAppearance.Car.Body1.Light"/>
-</FrameLayout> \ No newline at end of file
+ android:layout_gravity="center_vertical"
+ android:includeFontPadding="false"/>
+</FrameLayout>
diff --git a/res/layout/media_browse_list_item.xml b/res/layout/media_browse_list_item.xml
index c2f45e1..5e3e28f 100644
--- a/res/layout/media_browse_list_item.xml
+++ b/res/layout/media_browse_list_item.xml
@@ -14,45 +14,113 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<LinearLayout
+<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/car_keyline_1"
- android:layout_marginEnd="@dimen/car_keyline_1"
+ android:layout_height="@dimen/media_browse_list_item_height"
android:focusable="true"
- android:orientation="horizontal">
+ android:foreground="?android:attr/selectableItemBackground">
<ImageView
android:id="@+id/thumbnail"
- android:layout_width="@dimen/car_drawer_list_item_icon_size"
- android:layout_height="@dimen/car_drawer_list_item_icon_size"
- android:layout_marginEnd="@dimen/car_drawer_list_item_icon_end_margin"
- android:layout_gravity="center_vertical"
- android:scaleType="centerCrop" />
- <LinearLayout
- android:id="@+id/text_container"
+ android:layout_width="@dimen/media_browse_list_item_thumbnail_size"
+ android:layout_height="@dimen/media_browse_list_item_thumbnail_size"
+ android:layout_marginStart="@dimen/media_browse_list_item_icon_margin_start"
+ android:scaleType="centerCrop"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+
+ <!-- This guideline is necessary because there are icons preceding the text which typically have
+ visibility GONE, which prevents margins applied to the leftmost view from applying to the
+ TextViews constrained to that chain -->
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/text_start_guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_begin="@dimen/media_browse_list_item_text_margin_x"/>
+
+ <ImageView
+ android:id="@+id/download_icon_with_title"
+ android:layout_width="@dimen/media_browse_indicator_size"
+ android:layout_height="@dimen/media_browse_indicator_size"
+ android:src="@drawable/ic_file_download_done_black"
+ android:tint="@color/icon_tint"
+ app:layout_constraintStart_toEndOf="@+id/text_start_guideline"
+ app:layout_constraintEnd_toStartOf="@+id/explicit_icon_with_title"
+ app:layout_constraintTop_toTopOf="@+id/title"
+ app:layout_constraintBottom_toBottomOf="@+id/title"/>
+
+ <ImageView
+ android:id="@+id/explicit_icon_with_title"
+ android:layout_width="@dimen/media_browse_indicator_size"
+ android:layout_height="@dimen/media_browse_indicator_size"
+ android:src="@drawable/ic_explicit_black"
+ android:tint="@color/icon_tint"
+ app:layout_constraintStart_toEndOf="@+id/download_icon_with_title"
+ app:layout_constraintEnd_toStartOf="@+id/title"
+ app:layout_constraintTop_toTopOf="@+id/title"
+ app:layout_constraintBottom_toBottomOf="@+id/title"/>
+
+ <TextView
+ android:id="@+id/title"
+ style="@style/BrowseListTitleStyle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:includeFontPadding="false"
+ android:singleLine="true"
+ app:layout_constraintVertical_chainStyle="packed"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toTopOf="@+id/subtitle"
+ app:layout_constraintStart_toEndOf="@+id/explicit_icon_with_title"
+ app:layout_constraintEnd_toStartOf="@+id/right_arrow"/>
+
+ <ImageView
+ android:id="@+id/download_icon_with_subtitle"
+ android:layout_width="@dimen/media_browse_indicator_size"
+ android:layout_height="@dimen/media_browse_indicator_size"
+ android:src="@drawable/ic_file_download_done_black"
+ android:tint="@color/icon_tint"
+ app:layout_constraintStart_toStartOf="@+id/download_icon_with_title"
+ app:layout_constraintEnd_toStartOf="@+id/explicit_icon_with_subtitle"
+ app:layout_constraintTop_toTopOf="@+id/subtitle"
+ app:layout_constraintBottom_toBottomOf="@+id/subtitle"/>
+
+ <ImageView
+ android:id="@+id/explicit_icon_with_subtitle"
+ android:layout_width="@dimen/media_browse_indicator_size"
+ android:layout_height="@dimen/media_browse_indicator_size"
+ android:src="@drawable/ic_explicit_black"
+ android:tint="@color/icon_tint"
+ app:layout_constraintStart_toEndOf="@+id/download_icon_with_subtitle"
+ app:layout_constraintEnd_toStartOf="@+id/subtitle"
+ app:layout_constraintTop_toTopOf="@+id/subtitle"
+ app:layout_constraintBottom_toBottomOf="@+id/subtitle"/>
+
+ <TextView
+ android:id="@+id/subtitle"
+ style="@style/BrowseListSubtitleStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
- android:layout_weight="1"
+ android:includeFontPadding="false"
+ android:singleLine="true"
+ app:layout_constraintTop_toBottomOf="@+id/title"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/explicit_icon_with_subtitle"
+ app:layout_constraintEnd_toStartOf="@+id/right_arrow"/>
+
+ <ImageView
+ android:id="@+id/right_arrow"
+ android:src="@drawable/ic_chevron_right"
+ android:layout_width="@dimen/media_browse_list_item_arrow_size"
+ android:layout_height="@dimen/media_browse_list_item_arrow_size"
android:layout_gravity="center_vertical"
- android:orientation="vertical" >
- <TextView
- android:id="@+id/title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginBottom="@dimen/car_padding_1"
- android:includeFontPadding="false"
- android:ellipsize="end"
- android:maxLines="1"
- android:textAppearance="@style/TextAppearance.Car.Body1.Light" />
- <TextView
- android:id="@+id/subtitle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:includeFontPadding="false"
- android:ellipsize="end"
- android:maxLines="1"
- android:textAppearance="@style/TextAppearance.Car.Body2.Light" />
- </LinearLayout>
-</LinearLayout>
+ android:scaleType="centerCrop"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/title"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/res/layout/media_browse_panel_item.xml b/res/layout/media_browse_panel_item.xml
deleted file mode 100644
index db5dfd6..0000000
--- a/res/layout/media_browse_panel_item.xml
+++ /dev/null
@@ -1,56 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2018, The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/container"
- android:layout_width="match_parent"
- android:layout_height="@dimen/car_single_line_list_item_height"
- android:focusable="true"
- android:gravity="center"
- android:orientation="horizontal"
- android:layout_marginStart="@dimen/car_keyline_1"
- android:layout_marginEnd="@dimen/car_keyline_1">
- <ImageView
- android:id="@+id/thumbnail"
- android:layout_width="@dimen/car_primary_icon_size"
- android:layout_height="@dimen/car_primary_icon_size"
- android:layout_marginEnd="@dimen/car_drawer_list_item_icon_end_margin"
- android:layout_gravity="center_vertical"
- android:scaleType="centerCrop" />
- <LinearLayout
- android:id="@+id/text_container"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:layout_gravity="center_vertical"
- android:orientation="vertical" >
- <TextView
- android:id="@+id/title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginBottom="@dimen/car_text_vertical_margin"
- android:maxLines="1"
- android:textAppearance="?attr/drawerItemTitleTextAppearance" />
- <TextView
- android:id="@+id/subtitle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:ellipsize="end"
- android:maxLines="1"
- android:textAppearance="?attr/drawerItemBodyTextAppearance" />
- </LinearLayout>
-</LinearLayout> \ No newline at end of file
diff --git a/res/layout/media_browse_spacer.xml b/res/layout/media_browse_spacer.xml
new file mode 100644
index 0000000..298f5eb
--- /dev/null
+++ b/res/layout/media_browse_spacer.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<Space
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/spacer"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/browse_spacer_height"/> \ No newline at end of file
diff --git a/res/layout/metadata_compact.xml b/res/layout/metadata_compact.xml
deleted file mode 100644
index 7c4de7a..0000000
--- a/res/layout/metadata_compact.xml
+++ /dev/null
@@ -1,58 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2018, The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<merge
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools">
-
- <TextView
- android:id="@+id/title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginBottom="@dimen/car_padding_1"
- android:ellipsize="end"
- android:includeFontPadding="false"
- android:maxLines="1"
- android:textAppearance="@style/TextAppearance.Car.Body1.Light"
- tools:text="Body 1 Header"/>
- <TextView
- android:id="@+id/subtitle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_below="@+id/title"
- android:layout_marginBottom="@dimen/car_padding_1"
- android:ellipsize="end"
- android:includeFontPadding="false"
- android:maxLines="1"
- android:textAppearance="@style/TextAppearance.Car.Body2.Light"
- tools:text="Body 2"/>
- <SeekBar
- android:id="@+id/seek_bar"
- android:layout_width="match_parent"
- android:layout_height="@dimen/playback_seekbar_height"
- android:layout_below="@+id/subtitle"
- android:paddingEnd="0dp"
- android:paddingStart="0dp"
- android:progressDrawable="@drawable/seekbar_background"
- android:clickable="false"
- android:focusable="false"
- android:thumb="@drawable/seekbar_thumb"
- android:background="@null"
- android:visibility="invisible"
- tools:visibility="visible"
- tools:progress="70"/>
-
-</merge>
diff --git a/res/layout/metadata_normal.xml b/res/layout/metadata_normal.xml
index 687d59d..f5df6ed 100644
--- a/res/layout/metadata_normal.xml
+++ b/res/layout/metadata_normal.xml
@@ -17,66 +17,67 @@
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- android:id="@+id/metadata_subcontainer"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:focusable="false">
+ android:layout_height="wrap_content">
+
+ <ImageView
+ android:id="@+id/album_art"
+ android:layout_width="@dimen/playback_album_art_size"
+ android:layout_height="@dimen/playback_album_art_size"
+ android:contentDescription="@string/album_art"
+ android:background="@color/album_art_background"
+ android:scaleType="centerCrop"
+ android:transitionName="@string/album_art"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"/>
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
- android:ellipsize="end"
- android:includeFontPadding="false"
- android:maxLines="@integer/playback_title_text_max_lines"
- android:textAppearance="@style/TextAppearance.Car.Body1.Light"
+ android:layout_marginStart="@dimen/art_metadata_margin"
+ style="@style/MetadataPlaybackTitleStyle"
+ app:layout_constraintStart_toEndOf="@id/album_art"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- tools:text="Body 1 Header"/>
+ app:layout_constraintTop_toTopOf="@id/album_art"
+ app:layout_constraintBottom_toTopOf="@+id/artist"
+ app:layout_constraintVertical_chainStyle="packed"/>
+
<TextView
- android:id="@+id/subtitle"
+ android:id="@+id/artist"
android:layout_width="0dp"
android:layout_height="wrap_content"
- android:layout_marginTop="@dimen/car_padding_0"
- android:layout_marginEnd="@dimen/car_padding_5"
- android:ellipsize="end"
- android:maxLines="@integer/playback_subtitle_text_max_lines"
- android:textAppearance="@style/TextAppearance.Car.Body2.Light"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toStartOf="@+id/time"
- app:layout_constraintTop_toBottomOf="@+id/title"
- tools:text="Body 2"/>
+ android:layout_marginTop="@dimen/metadata_title_subtitle_margin"
+ style="@style/MetadataPlaybackSubtitleStyle"
+ app:layout_constraintStart_toStartOf="@id/title"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/title"
+ app:layout_constraintBottom_toTopOf="@+id/album_title"/>
+
<TextView
- android:id="@+id/time"
- android:layout_width="160dp"
+ android:id="@+id/album_title"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginTop="@dimen/car_padding_0"
- android:ellipsize="end"
- android:maxLines="@integer/playback_subtitle_text_max_lines"
- android:gravity="end"
- android:textAppearance="@style/TextAppearance.Car.Body2.Light"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/title"
- tools:text="3:27 / 4:03"/>
- <SeekBar
- android:id="@+id/seek_bar"
- android:layout_width="0dp"
- android:layout_height="@dimen/playback_seekbar_height"
- android:layout_marginTop="@dimen/car_padding_4"
- android:clickable="false"
- android:focusable="false"
- android:paddingEnd="0dp"
- android:paddingStart="0dp"
- android:progressDrawable="@drawable/seekbar_background"
- android:thumb="@drawable/seekbar_thumb"
- android:background="@null"
- android:visibility="invisible"
- app:layout_constraintStart_toStartOf="parent"
+ android:layout_marginTop="@dimen/metadata_subtitles_margin"
+ style="@style/MetadataPlaybackSubtitleStyle"
+ app:layout_goneMarginTop="@dimen/metadata_title_subtitle_margin"
+ app:layout_constraintStart_toStartOf="@id/title"
+ app:layout_constraintEnd_toStartOf="@+id/progress_text_container"
+ app:layout_constraintTop_toBottomOf="@id/artist"
+ app:layout_constraintBottom_toBottomOf="@id/album_art"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintHorizontal_bias="0.0"
+ app:layout_constrainedWidth="true"/>
+
+ <include
+ android:id="@+id/progress_text_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintStart_toEndOf="@+id/album_title"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/subtitle"
- tools:progress="70"
- tools:visibility="visible"/>
+ app:layout_constraintTop_toTopOf="@+id/album_title"
+ app:layout_constraintBottom_toBottomOf="@+id/album_title"
+ layout="@layout/time_progress_text"/>
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/playback_controls.xml b/res/layout/playback_controls.xml
deleted file mode 100644
index cbd41a7..0000000
--- a/res/layout/playback_controls.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2018, The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<com.android.car.media.common.PlaybackControlsActionBar
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/playback_controls"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginHorizontal="@dimen/car_margin"
- android:layout_marginBottom="@dimen/car_padding_1"
- app:columns="5"
- app:layout_constraintBottom_toBottomOf="parent"/>
diff --git a/res/layout/queue_list_item.xml b/res/layout/queue_list_item.xml
new file mode 100644
index 0000000..536649e
--- /dev/null
+++ b/res/layout/queue_list_item.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2019, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/queue_list_item_height"
+ android:paddingEnd="@dimen/queue_list_item_padding_x"
+ android:gravity="center_vertical"
+ android:background="?android:attr/selectableItemBackground">
+
+ <FrameLayout
+ android:id="@+id/thumbnail_container"
+ android:layout_width="@dimen/queue_list_item_thumbnail_container_width"
+ android:layout_height="wrap_content"
+ android:paddingStart="@dimen/queue_list_item_padding_x">
+ <ImageView
+ android:id="@+id/thumbnail"
+ android:layout_width="@dimen/queue_list_item_thumbnail_size"
+ android:layout_height="@dimen/queue_list_item_thumbnail_size"
+ android:scaleType="centerCrop"/>
+ </FrameLayout>
+
+ <Space
+ android:id="@+id/spacer"
+ android:layout_width="@dimen/queue_list_item_spacer_width"
+ android:layout_height="wrap_content"/>
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ style="@style/QueueListItemTitleStyle"/>
+
+ <TextView
+ android:id="@+id/current_time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/queue_list_item_title_time_margin"
+ style="@style/QueueListItemTimeStyle"/>
+
+ <TextView
+ android:id="@+id/separator"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/slash_separator"
+ style="@style/QueueListItemTimeStyle"/>
+
+ <TextView
+ android:id="@+id/max_time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="@style/QueueListItemTimeStyle"/>
+
+ <ImageView
+ android:id="@+id/now_playing_icon"
+ android:src="@drawable/ic_equalizer"
+ android:layout_width="@dimen/media_browse_list_item_arrow_size"
+ android:layout_height="@dimen/media_browse_list_item_arrow_size"
+ android:layout_gravity="center_vertical"
+ android:scaleType="centerCrop"/>
+
+</LinearLayout>
diff --git a/res/layout/search_bar.xml b/res/layout/search_bar.xml
new file mode 100644
index 0000000..43041d9
--- /dev/null
+++ b/res/layout/search_bar.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<merge
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <com.android.car.media.common.MediaAppSelectorWidget
+ android:id="@+id/app_icon_container"
+ android:layout_width="@dimen/appbar_view_icon_touch_target_size"
+ android:layout_height="@dimen/appbar_view_icon_touch_target_size"
+ android:padding="@dimen/app_switch_widget_icon_padding"
+ android:orientation="horizontal"
+ android:gravity="center"
+ android:background="@drawable/appbar_view_icon_background"
+ app:switchingEnabled="false"/>
+
+ <EditText
+ android:id="@+id/search_bar"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:paddingLeft="@dimen/appbar_view_icon_touch_target_size"
+ android:layout_marginLeft="@dimen/appbar_view_search_margin"
+ android:hint="@string/search_hint"
+ android:textColorHint="@color/search_hint_text_color"
+ android:inputType="text"
+ android:singleLine="true"
+ android:imeOptions="actionDone"
+ android:cursorVisible="false"/>
+
+ <ImageView
+ android:id="@+id/search_close"
+ android:layout_width="@dimen/appbar_view_icon_touch_target_size"
+ android:layout_height="@dimen/appbar_view_icon_touch_target_size"
+ android:layout_marginLeft="@dimen/appbar_view_search_margin"
+ android:padding="@dimen/appbar_view_icon_padding"
+ android:background="@drawable/appbar_view_icon_background"
+ android:src="@drawable/ic_close"/>
+</merge> \ No newline at end of file
diff --git a/res/layout/tab_view.xml b/res/layout/tab_view.xml
deleted file mode 100644
index feafeb1..0000000
--- a/res/layout/tab_view.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2018, The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<merge
- xmlns:android="http://schemas.android.com/apk/res/android">
-
- <ImageView
- android:id="@+id/icon"
- android:layout_width="@dimen/car_primary_icon_size"
- android:layout_height="@dimen/car_primary_icon_size"
- android:layout_marginBottom="@dimen/car_padding_1"
- android:scaleType="fitCenter"
- android:layout_gravity="center_horizontal"
- android:tint="@color/car_body5_light"/>
-
- <TextView
- android:id="@+id/title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:maxLines="1"
- android:maxWidth="@dimen/browse_tab_width"
- android:ellipsize="end"
- android:includeFontPadding="false"
- android:textAppearance="@style/TextAppearance.Car.Body5.Light"/>
-
-</merge> \ No newline at end of file
diff --git a/res/layout/time_progress_text.xml b/res/layout/time_progress_text.xml
new file mode 100644
index 0000000..a359994
--- /dev/null
+++ b/res/layout/time_progress_text.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2019, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/progress_text_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/outer_separator"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/dash_separator"
+ style="@style/MetadataPlaybackSubtitleStyle"
+ />
+
+ <TextView
+ android:id="@+id/current_time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="@style/MetadataPlaybackSubtitleStyle"
+ />
+
+ <TextView
+ android:id="@+id/inner_separator"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/slash_separator"
+ style="@style/MetadataPlaybackSubtitleStyle"
+ />
+
+ <TextView
+ android:id="@+id/max_time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="@style/MetadataPlaybackSubtitleStyle"
+ />
+</LinearLayout>
diff --git a/res/transition/queue_in.xml b/res/transition/queue_in.xml
deleted file mode 100644
index 6761f50..0000000
--- a/res/transition/queue_in.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2018, The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
- android:transitionOrdering="sequential">
- <arcMotion/>
- <fade android:fadingMode="fade_out"/>
- <transitionSet android:transitionOrdering="together">
- <changeBounds>
- <targets>
- <target android:targetId="@+id/album_art"/>
- </targets>
- </changeBounds>
- <changeBounds android:startDelay="50">
- <targets>
- <target android:excludeId="@+id/album_art"/>
- </targets>
- </changeBounds>
- </transitionSet>
- <fade android:fadingMode="fade_in"/>
-</transitionSet>
diff --git a/res/transition/queue_out.xml b/res/transition/queue_out.xml
deleted file mode 100644
index 052d216..0000000
--- a/res/transition/queue_out.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2018, The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
- android:transitionOrdering="sequential">
- <arcMotion/>
- <fade android:fadingMode="fade_out"/>
- <transitionSet android:transitionOrdering="together">
- <changeBounds android:startDelay="100">
- <targets>
- <target android:targetId="@+id/album_art"/>
- </targets>
- </changeBounds>
- <changeBounds>
- <targets>
- <target android:excludeId="@+id/album_art"/>
- </targets>
- </changeBounds>
- </transitionSet>
- <fade android:fadingMode="fade_in"/>
-</transitionSet>
diff --git a/res/values-wheel/bools.xml b/res/values-night/dimens.xml
index cd123b4..33f1da9 100644
--- a/res/values-wheel/bools.xml
+++ b/res/values-night/dimens.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright 2016, The Android Open Source Project
+ Copyright 2019, The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,5 +15,6 @@
limitations under the License.
-->
<resources>
- <bool name="has_wheel">true</bool>
+ <!-- Music -->
+ <item name="media_background_alpha" format="float" type="dimen">0.84</item>
</resources>
diff --git a/res/values-port/dimens.xml b/res/values-port/dimens.xml
new file mode 100644
index 0000000..e1518d6
--- /dev/null
+++ b/res/values-port/dimens.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2019, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <!-- browse_playback_background.xml -->
+ <!-- Scale factor for the background image -->
+ <dimen name="browse_playback_album_art_margin_start">5dp</dimen>
+ <dimen name="browse_playback_album_art_margin_end">10dp</dimen>
+
+ <!-- appbar_view.xml -->
+ <dimen name="browse_fragment_top_padding">@dimen/appbar_2_rows_height</dimen>
+ <dimen name="browse_fragment_top_padding_stacked">@dimen/appbar_first_row_height</dimen>
+
+ <!-- x-axis margin of playback seekbar -->
+ <dimen name="playback_seekbar_margin_x">@*android:dimen/car_padding_5</dimen>
+</resources>
diff --git a/res/values-h1200dp/bools.xml b/res/values-port/integers.xml
index b1476c8..3f4641d 100644
--- a/res/values-h1200dp/bools.xml
+++ b/res/values-port/integers.xml
@@ -15,6 +15,7 @@
limitations under the License.
-->
<resources>
- <!-- On screens tall enough we enable content forward browsing -->
- <bool name="forward_content_browse_enabled">true</bool>
-</resources> \ No newline at end of file
+ <!-- Number of rows in the app bar view -->
+ <integer name="num_app_bar_view_rows">2</integer>
+ <integer name="num_browse_columns">3</integer>
+</resources>
diff --git a/res/values/attrs.xml b/res/values-port/styles.xml
index 134f7f5..3c80a39 100644
--- a/res/values/attrs.xml
+++ b/res/values-port/styles.xml
@@ -1,22 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright 2018, The Android Open Source Project
+ Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
+ http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
--->
+ -->
+
<resources>
- <declare-styleable name="AppBarView">
- <!-- Maximum number of tabs to show on the app bar -->
- <attr name="max_tabs" format="integer"/>
- </declare-styleable>
+ <style name="AppBarTabStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">@dimen/appbar_second_row_height</item>
+ <item name="layout_constraintTop_toBottomOf">@+id/row_separator</item>
+ <item name="layout_constraintBottom_toBottomOf">parent</item>
+ </style>
</resources>
diff --git a/res/values-w1024dp/dimens.xml b/res/values-w1024dp/dimens.xml
deleted file mode 100644
index 12da79d..0000000
--- a/res/values-w1024dp/dimens.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 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.
--->
-<resources>
- <!-- Music -->
- <dimen name="apps_max_content_width">748dp</dimen>
-
- <!-- Music now playing screen -->
- <dimen name="music_action_icon_inset">0dp</dimen>
- <dimen name="music_action_ripple_inset">32dp</dimen>
-
- <!-- action card -->
- <dimen name="metadata_width">720dp</dimen>
- <dimen name="metadata_inter_line_space">8dp</dimen>
-
- <dimen name="music_panel_elevation">2dp</dimen>
-
- <dimen name="play_pause_elevation">6dp</dimen>
-
- <dimen name="controls_tap_target_width">188dp</dimen>
- <dimen name="controls_tap_target_height">128dp</dimen>
-
- <dimen name="overflow_margin">46dp</dimen>
- <dimen name="overflow_spacing_outer">49dp</dimen>
- <dimen name="overflow_spacing_inner">48dp</dimen>
-
- <dimen name="seek_bar_height">8dp</dimen>
-</resources>
diff --git a/res/values-w1280dp/dimens.xml b/res/values-w1280dp/dimens.xml
new file mode 100644
index 0000000..36ba2c0
--- /dev/null
+++ b/res/values-w1280dp/dimens.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <!-- BrowseFragment.xml -->
+ <!-- On wide screens, there is an additional margin below the minimized control bar.
+ Since you can't sum dimensions in xml, this value is:
+ @dimen/minimized_control_bar_height + @dimen/minimized_control_bar_margin_bottom -->
+ <dimen name="browse_fragment_bottom_padding">144dp</dimen>
+</resources>
diff --git a/res/values-w748dp/dimens.xml b/res/values-w1280dp/integers.xml
index be40cc9..f4873dc 100644
--- a/res/values-w748dp/dimens.xml
+++ b/res/values-w1280dp/integers.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright 2016, The Android Open Source Project
+ Copyright 2019, The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,6 +15,6 @@
limitations under the License.
-->
<resources>
- <!-- The max width of content in apps for adaptive responsive -->
- <dimen name="apps_max_content_width">748dp</dimen>
+ <!-- Number of columns in the browse view -->
+ <integer name="num_browse_columns">4</integer>
</resources>
diff --git a/res/values-w1280dp/styles.xml b/res/values-w1280dp/styles.xml
new file mode 100644
index 0000000..abf644b
--- /dev/null
+++ b/res/values-w1280dp/styles.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2019, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <style name="MetadataContainerStyle">
+ <item name="android:layout_width">1028dp</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_marginHorizontal">@dimen/fragment_metadata_margin_x</item>
+ </style>
+
+ <style name="SeekBarStyle">
+ <item name="android:layout_width">1028dp</item>
+ <item name="android:layout_height">@dimen/playback_seekbar_height</item>
+ </style>
+</resources>
diff --git a/res/values/bools.xml b/res/values/bools.xml
index 3247ad9..2cbdcb8 100644
--- a/res/values/bools.xml
+++ b/res/values/bools.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright 2018, The Android Open Source Project
+ Copyright 2019, The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,6 +15,10 @@
limitations under the License.
-->
<resources>
- <!-- Whether forward content browse is enabled -->
- <bool name="forward_content_browse_enabled">false</bool>
-</resources> \ No newline at end of file
+ <bool name="queue_fading_edge_length_enabled">true</bool>
+ <bool name="use_media_source_color_for_progress_bar">true</bool>
+ <bool name="show_time_for_now_playing_queue_list_item">true</bool>
+ <bool name="show_icon_for_now_playing_queue_list_item">false</bool>
+ <bool name="switch_to_playback_view_when_playable_item_is_clicked">true</bool>
+ <bool name="show_thumbnail_for_queue_list_item">true</bool>
+</resources>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 41ebd6e..d7a6046 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -17,23 +17,37 @@
<resources>
<color name="no_content_text_color">#ffffff</color>
- <color name="car_body5_light">@android:color/white</color>
- <color name="car_body5_dark">@android:color/black</color>
- <color name="car_body5">@color/car_body5_dark</color>
+ <color name="icon_tint">@*android:color/car_body2_light</color>
+
+ <color name="album_art_background">@*android:color/car_grey_100</color>
<!-- Color displayed behind the transport controls while browsing -->
- <color name="browse_playback_bg">@android:color/black</color>
+ <color name="browse_playback_bg_color">@*android:color/car_card</color>
<!-- Default media background -->
- <color name="media_template_background">@color/car_dark_blue_grey_800</color>
+ <color name="media_template_background">@*android:color/car_dark_blue_grey_800</color>
<!-- Scrim displayed on top of playback album art background -->
- <color name="media_scrim_background">@color/car_dark_blue_grey_900</color>
+ <color name="media_scrim_background">@*android:color/car_dark_blue_grey_900</color>
<!-- Color used on the progress bar background -->
- <color name="progress_bar_background">@color/car_grey_900</color>
+ <color name="progress_bar_background">@*android:color/car_seekbar_track_background</color>
<!-- Color used on the progress bar -->
- <color name="progress_bar_highlight">#fafafa</color>
+ <color name="progress_bar_highlight">@color/media_source_default_color</color>
+ <!-- Color used on the thumb of the progress bar -->
+ <color name="progress_bar_thumb_color">@color/media_source_default_color</color>
+
+ <!-- appbar_view.xml -->
+ <color name="appbar_view_icon_tint">@color/primary_app_icon_color</color>
+ <color name="appbar_view_settings_tint">@color/uxr_button_image_color_selector</color>
+ <color name="search_hint_text_color">#33FFFFFF</color>
+
+ <!-- fragment_metadata.xml -->
+ <color name="fragment_metadata_queue_divider_color">@*android:color/car_list_divider_light
+ </color>
+ <color name="queue_playing_icon_color">@*android:color/car_tint_light</color>
+
+ <!-- tab_view.xml -->
+ <color name="tab_view_icon_tint">@*android:color/car_tint_light</color>
+ <color name="tab_view_text_color">@*android:color/car_grey_400</color>
- <!-- App selection backgroun -->
- <color name="alpha_fragment_background_color">#ea000000</color>
- <!-- App selection items background -->
- <color name="app_item_background_color">#66ffffff</color>
+ <!-- fragment_error.xml -->
+ <color name="error_button_text_color">@color/uxr_button_text_color_selector</color>
</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index f4a7439..5231544 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -18,6 +18,7 @@
<!-- Music -->
<item name="media_scrim_alpha" format="float" type="dimen">0.6</item>
<item name="media_scrim_darkened_alpha" format="float" type="dimen">0.9</item>
+ <item name="media_background_alpha" format="float" type="dimen">0.78</item>
<dimen name="apps_max_content_width">748dp</dimen>
<!-- Music now playing screen -->
@@ -33,26 +34,36 @@
<dimen name="controls_spacing_outer">81dp</dimen>
<!-- Playback seekbar height -->
- <dimen name="playback_seekbar_height">8dp</dimen>
+ <dimen name="playback_seekbar_height">76dp</dimen>
+ <!-- Playback seekbar track height -->
+ <dimen name="playback_seekbar_track_height">4dp</dimen>
<!-- Size of the thumb in the playback seekbar -->
- <dimen name="playback_seekbar_thumb_width">6dp</dimen>
+ <dimen name="playback_seekbar_thumb_height">16dp</dimen>
+ <dimen name="playback_seekbar_thumb_width">16dp</dimen>
+ <dimen name="playback_seekbar_thumb_offset">0px</dimen>
+ <!-- Paddings of playback seekbar -->
+ <dimen name="playback_seekbar_padding_x">0dp</dimen>
+ <!-- x-axis margin of playback seekbar -->
+ <dimen name="playback_seekbar_margin_x">@*android:dimen/car_margin</dimen>
<!-- Image size used to generate the background -->
<dimen name="playback_background_raw_image_size">300px</dimen>
+
+
<!-- Blurring radius used to blur background images -->
<item name="playback_background_blur_radius" format="float" type="dimen">25</item>
<!-- Scale to apply to images before blurring them to create the playback background -->
- <item name="playback_background_blur_scale" format="float" type="dimen">1</item>
+ <item name="playback_background_blur_scale" format="float" type="dimen">.5</item>
<!-- Size of the album art thumbnail -->
- <dimen name="playback_album_art_size_normal">156dp</dimen>
- <!-- Size of the album art thumbnail when large -->
- <dimen name="playback_album_art_size_large">574dp</dimen>
- <!-- Bottom marging for the playback queue -->
- <dimen name="playback_controls_margin">192dp</dimen>
+ <dimen name="playback_album_art_size">226dp</dimen>
+ <!-- End margin of playback queue button -->
+ <dimen name="playback_queue_button_margin_end">@*android:dimen/car_padding_2</dimen>
<!-- Tab height -->
<dimen name="browse_tab_height">76dp</dimen>
<!-- Tab max width -->
<dimen name="browse_tab_width">134dp</dimen>
+ <!-- Tab Padding -->
+ <dimen name="browse_tab_padding">@*android:dimen/car_padding_4</dimen>
<!-- Browse playback controls size -->
<dimen name="browse_playback_controls_height">192dp</dimen>
<!-- Selected tab alpha -->
@@ -60,6 +71,112 @@
<!-- Unselected tab alpha -->
<item name="browse_tab_alpha_unselected" format="float" type="dimen">0.5</item>
- <!-- App bar -->
- <dimen name="appbar_height">116dp</dimen>
+ <!-- appbar_view.xml -->
+ <dimen name="appbar_view_nav_button_width">@*android:dimen/car_margin</dimen>
+ <dimen name="appbar_view_tabs_margin_start">@*android:dimen/car_padding_4</dimen>
+ <dimen name="appbar_view_tabs_margin_end">@*android:dimen/car_padding_3</dimen>
+ <dimen name="appbar_view_control_buttons_spacing">@*android:dimen/car_padding_3</dimen>
+ <dimen name="appbar_view_control_buttons_margin_end">@*android:dimen/car_padding_2</dimen>
+ <dimen name="appbar_view_title_margin_start">@*android:dimen/car_padding_2</dimen>
+ <!-- negative margin to overlap icons on search bar -->
+ <dimen name="appbar_view_search_margin">-96dp</dimen>
+
+ <!-- browse_state.xml -->
+ <dimen name="browse_state_progress_height">@*android:dimen/car_double_line_list_item_height
+ </dimen>
+ <dimen name="browse_state_error_margin_top">@*android:dimen/car_padding_4</dimen>
+
+ <!-- media_activity.xml -->
+ <dimen name="media_activity_controls_container_padding">@*android:dimen/car_padding_4</dimen>
+ <dimen name="media_activity_controls_margin_start">@*android:dimen/car_padding_4</dimen>
+
+ <!-- BrowseFragment.java -->
+ <!-- Spacer used between the app bar and the top of the browse list/grid -->
+ <dimen name="browse_spacer_height">@*android:dimen/car_padding_2</dimen>
+ <dimen name="grid_item_spacing">@*android:dimen/car_padding_4</dimen>
+ <dimen name="grid_item_margin_x">0dp</dimen>
+
+ <!-- media_browse_grid_item.xml -->
+ <dimen name="media_browse_grid_item_margin_bottom">@*android:dimen/car_padding_4</dimen>
+ <dimen name="media_browse_grid_item_text_margin_top">@*android:dimen/car_padding_3</dimen>
+ <dimen name="media_browse_grid_item_padding">12dp</dimen>
+ <dimen name="media_browse_grid_item_background_radius">4dp</dimen>
+
+ <!-- media_browse_header_item.xml -->
+ <dimen name="media_browse_header_item_height">76dp</dimen>
+ <dimen name="media_browse_header_item_margin_x">0dp</dimen>
+
+ <!-- media_browse_list_item.xml -->
+ <dimen name="media_browse_list_item_height">116dp</dimen>
+ <dimen name="media_browse_list_item_thumbnail_size">76dp</dimen>
+ <dimen name="media_browse_list_item_text_margin_x">@*android:dimen/car_keyline_4</dimen>
+ <dimen name="media_browse_list_item_icon_margin_start">@*android:dimen/car_keyline_1</dimen>
+ <dimen name="media_browse_list_item_arrow_size">@dimen/touch_target_size</dimen>
+
+ <!-- media_browse_panel_item.xml -->
+ <dimen name="media_browse_panel_item_height">@*android:dimen/car_single_line_list_item_height
+ </dimen>
+ <dimen name="media_browse_panel_item_margin_x">@*android:dimen/car_keyline_1</dimen>
+ <dimen name="media_browse_panel_item_icon_size">@*android:dimen/car_primary_icon_size</dimen>
+ <dimen name="media_browse_panel_item_icon_margin_end">32dp</dimen>
+ <dimen name="media_browse_panel_item_title_margin_bottom">2dp</dimen>
+
+ <!-- media_browse_icon.xml-->
+ <dimen name="media_browse_indicator_size">25dp</dimen>
+
+ <!-- metadata_normal.xml -->
+ <dimen name="metadata_title_subtitle_margin">@*android:dimen/car_padding_4</dimen>
+ <dimen name="metadata_subtitles_margin">@*android:dimen/car_padding_2</dimen>
+ <dimen name="art_metadata_margin">48dp</dimen>
+
+ <!-- browse_playback_background.xml -->
+ <dimen name="browse_playback_background_radius">@*android:dimen/car_radius_2</dimen>
+ <dimen name="browse_playback_album_art_size">96dp</dimen>
+ <dimen name="browse_playback_album_art_margin_start">@*android:dimen/car_padding_2</dimen>
+ <dimen name="browse_playback_album_art_margin_end">@*android:dimen/car_padding_4</dimen>
+ <dimen name="browse_playback_text_margin_bottom">@*android:dimen/car_padding_1</dimen>
+
+ <!-- tab_view.xml -->
+ <dimen name="tab_view_icon_size">@*android:dimen/car_primary_icon_size</dimen>
+ <dimen name="tab_view_icon_margin_bottom">@*android:dimen/car_padding_1</dimen>
+
+ <!-- fragment_error.xml -->
+ <dimen name="fragment_error_margin_top">@*android:dimen/car_padding_4</dimen>
+ <dimen name="fragment_error_button_margin_top">80dp</dimen>
+ <dimen name="fragment_error_message_margin_side">80dp</dimen>
+
+ <!-- fragment_playback.xml -->
+ <!-- The length of playback queue fading edge, equals 0.1 * window_height. -->
+ <dimen name="queue_fading_edge_length">81dp</dimen>
+ <dimen name="fragment_metadata_queue_divider_margin">@*android:dimen/car_keyline_1</dimen>
+ <dimen name="fragment_playback_queue_overlap_top">@*android:dimen/car_padding_4</dimen>
+ <dimen name="fragment_playback_queue_overlap_bottom">@*android:dimen/car_padding_4</dimen>
+ <dimen name="fragment_metadata_margin_x">@*android:dimen/car_margin</dimen>
+ <dimen name="playback_title_margin_end">@*android:dimen/car_padding_3</dimen>
+
+ <!-- PlaybackFragment.java -->
+ <dimen name="playback_queue_list_padding_top">@*android:dimen/car_padding_4</dimen>
+
+ <!-- queue_list_item.xml -->
+ <dimen name="queue_list_item_height">116dp</dimen>
+ <dimen name="queue_list_item_padding_x">@*android:dimen/car_keyline_1</dimen>
+ <dimen name="queue_list_item_title_time_margin">@*android:dimen/car_padding_4</dimen>
+ <dimen name="queue_list_item_thumbnail_container_width">@*android:dimen/car_keyline_4</dimen>
+ <dimen name="queue_list_item_thumbnail_size">76dp</dimen>
+ <dimen name="queue_list_item_spacer_width">@dimen/queue_list_item_padding_x</dimen>
+
+ <!-- fragment_browse.xml -->
+ <dimen name="browse_fragment_top_padding">@dimen/appbar_first_row_height</dimen>
+ <dimen name="browse_fragment_bottom_padding">@dimen/minimized_control_bar_height</dimen>
+ <!-- Top padding to use when at browse levels deeper than the first level -->
+ <dimen name="browse_fragment_top_padding_stacked">@dimen/appbar_first_row_height</dimen>
+ <item name="playback_queue_background_alpha" format="float" type="dimen">0.6</item>
+
+ <!-- fragment_search.xml -->
+ <dimen name="search_fragment_top_padding">@dimen/appbar_first_row_height</dimen>
+ <dimen name="search_fragment_bottom_padding">@dimen/minimized_control_bar_height</dimen>
+
+ <!-- ic_queue_button.xml -->
+ <dimen name="queue_button_background_size">54dp</dimen>
+
</resources>
diff --git a/res/values/integers.xml b/res/values/integers.xml
index d0f23ea..b17d7e0 100644
--- a/res/values/integers.xml
+++ b/res/values/integers.xml
@@ -21,18 +21,27 @@
<!-- The maximum number of lines for the artist of a currently playing media item. -->
<integer name="media_artist_max_lines">1</integer>
- <!-- Maximum number of browse tabs -->
- <integer name="max_browse_tabs">4</integer>
-
<!-- Number of columns in the browse view -->
<integer name="num_browse_columns">3</integer>
- <!-- Number of columns in app selector -->
- <integer name="num_app_selector_columns">3</integer>
-
<!-- The amount of time it takes for the app selector to fade in and out -->
<integer name="app_selector_fade_duration">500</integer>
+ <!-- The amount of time it takes for the fragment playback queue to fade in and out -->
+ <integer name="fragment_playback_queue_fade_duration_ms">500</integer>
+
<!-- Time allowed for a process to complete before we show a progress indicator -->
<integer name="progress_indicator_delay">1000</integer>
+
+ <!-- Maximum number of tabs to show on the app bar -->
+ <integer name="max_tabs">4</integer>
+
+ <!-- Number of rows in the app bar view -->
+ <integer name="num_app_bar_view_rows">1</integer>
+
+ <!-- Views to hide when opening the custom actions when the queue isn't visible. -->
+ <integer-array name="playback_views_to_hide_when_showing_custom_actions">
+ <item>@id/seek_bar</item>
+ </integer-array>
+
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index bdc7e92..c5d557c 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -16,18 +16,24 @@
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- Music -->
- <!-- Prompt text to display when the user hasn't picked any songs to play yet. [CHAR LIMIT=57]-->
- <string name="nothing_to_play">This folder is empty.</string>
+ <!-- Displayed while a folder selected by the user is being retrieved. [CHAR LIMIT=57] -->
+ <string name="browser_loading">Loading content…</string>
+ <!-- Displayed when a folder selected by the user doesn't have any content. [CHAR LIMIT=57] -->
+ <string name="nothing_to_play">No media available here.</string>
<!-- Prompt text to display when we failed to connect to a media app. [CHAR LIMIT=50]-->
- <string name="cannot_connect_to_app"><xliff:g name="app">%s</xliff:g> doesn\'t seem to be working right now.</string>
+ <string name="cannot_connect_to_app"><xliff:g name="app">%s</xliff:g> isn’t working right now.</string>
<!-- Prompt text to display when connecting to a media app. [CHAR LIMIT=50] -->
<string name="unknown_media_provider_name">Unknown</string>
- <!-- The text for pending user action. [CHAR LIMIT=50] -->
- <string name="loading">Getting your selection&#8230;</string>
<!-- The text for unknown playback error. [CHAR LIMIT=50] -->
<string name="unknown_error">Something went wrong</string>
<!-- Text of the button displayed at the bottom of a media browse section to provide access to additional items. [CHAR LIMIT=100] -->
- <string name="media_browse_more">More …</string>
+ <string name="media_browse_more">More…</string>
<!-- Media template title -->
<string name="media_app_title">Media</string>
+ <!-- Search input hint text. [CHAR LIMIT=20] -->
+ <string name="search_hint">Search songs, artists, and more...</string>
+ <!-- Error Fragment error message [CHAR LIMIT=100] -->
+ <string name="default_error_message">Unknown application error</string>
+ <!-- Title to display when in playback view. [CHAR LIMIT=50] -->
+ <string name="fragment_playback_title">Now Playing</string>
</resources>
diff --git a/res/values-w768dp/dimens.xml b/res/values/strings_no_translation.xml
index 15ba776..0439312 100644
--- a/res/values-w768dp/dimens.xml
+++ b/res/values/strings_no_translation.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright 2016, The Android Open Source Project
+ Copyright 2019, The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,7 +14,12 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
+
+<!-- This file is intended for non translatable string resources. -->
<resources>
- <!-- The max width of content in apps for adaptive responsive -->
- <dimen name="apps_max_content_width">768dp</dimen>
-</resources>
+ <!-- The package name of the media application that will be selected as the default. -->
+ <string name="default_media_application" translatable="false">com.android.bluetooth</string>
+
+ <string name="dash_separator" translatable="false">" - "</string>
+ <string name="slash_separator" translatable="false">" / "</string>
+</resources> \ No newline at end of file
diff --git a/res/values/styles.xml b/res/values/styles.xml
index f58306e..cb0f4ad 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -15,24 +15,78 @@
limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android">
- <style name="AppTheme" parent="android:Theme.Material.Light.NoActionBar">
- <item name="android:colorControlHighlight">@color/car_card_ripple_background_light</item>
- <item name="android:windowBackground">@null</item>
- </style>
-
<style name="TextAppearance.NoContent" parent="android:TextAppearance.Medium">
<item name="android:fontFamily">sans-serif-condensed</item>
- <item name="android:textSize">@dimen/car_body2_size</item>
+ <item name="android:textSize">@*android:dimen/car_body3_size</item>
<item name="android:textColor">@color/no_content_text_color</item>
</style>
- <style name="TextAppearance.Car.Body5">
- <item name="android:textStyle">normal</item>
- <item name="android:textSize">@dimen/car_body5_size</item>
- <item name="android:textColor">@color/car_body5</item>
+ <style name="AppBarTitleStyle">
+ <item name="android:textAppearance">@style/TextAppearance.Body1</item>
+ <item name="android:background">@drawable/media_app_title_background</item>
+ <item name="android:layout_width">0dp</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:singleLine">true</item>
+ </style>
+
+ <style name="AppBarTabStyle">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">@dimen/appbar_first_row_height</item>
+ <item name="android:layout_marginStart">@dimen/appbar_view_tabs_margin_start</item>
+ <item name="android:layout_marginEnd">@dimen/appbar_view_tabs_margin_end</item>
+ <item name="layout_constraintTop_toTopOf">parent</item>
+ <item name="layout_constraintBottom_toTopOf">@+id/row_separator</item>
+ <item name="layout_constraintStart_toStartOf">parent</item>
+ <item name="layout_constraintEnd_toStartOf">@+id/search</item>
+ <item name="layout_constraintHorizontal_bias">0.0</item>
+ <item name="layout_constrainedWidth">true</item>
+ </style>
+
+ <style name="MetadataPlaybackTitleStyle" parent="TextAppearance.Display2">
+ <item name="android:maxLines">2</item>
+ <item name="android:ellipsize">end</item>
+ <item name="android:lineSpacingExtra">@*android:dimen/car_padding_1</item>
+ <item name="android:includeFontPadding">false</item>
+ </style>
+
+ <style name="MetadataPlaybackSubtitleStyle" parent="TextAppearance.Body1">
+ <item name="android:textColor">@color/secondary_text_color</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:includeFontPadding">false</item>
+ </style>
+
+ <style name="MetadataContainerStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_marginHorizontal">@dimen/fragment_metadata_margin_x</item>
+ </style>
+
+ <style name="BrowseSubheaderStyle" parent="TextAppearance.Body3"/>
+ <style name="BrowseListTitleStyle" parent="TextAppearance.Body1"/>
+ <style name="BrowseListSubtitleStyle" parent="TextAppearance.Body3"/>
+ <style name="BrowseGridTitleStyle" parent="TextAppearance.Body2"/>
+ <style name="BrowseGridSubtitleStyle" parent="TextAppearance.Body3"/>
+
+ <style name="ErrorTextStyle" parent="TextAppearance.Body3"/>
+ <style name="ErrorButtonStyle" parent="TextAppearance.Body3">
+ <item name="android:textStyle">bold</item>
+ </style>
+
+ <style name="SeekBarStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">@dimen/playback_seekbar_height</item>
+ </style>
+
+ <style name="QueueListItemTitleStyle" parent="TextAppearance.Body1">
+ <item name="android:singleLine">true</item>
+ <item name="android:includeFontPadding">false</item>
+ <item name="android:gravity">center_vertical</item>
</style>
- <style name="TextAppearance.Car.Body5.Light">
- <item name="android:textColor">@color/car_body5_light</item>
+ <style name="QueueListItemTimeStyle" parent="TextAppearance.Body3">
+ <item name="android:singleLine">true</item>
+ <item name="android:includeFontPadding">false</item>
+ <item name="android:gravity">center_vertical</item>
+ <item name="android:textStyle">bold</item>
</style>
</resources>
diff --git a/res/values/themes.xml b/res/values/themes.xml
index 96a1a65..ac4d809 100644
--- a/res/values/themes.xml
+++ b/res/values/themes.xml
@@ -14,11 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
- <!-- The theme for the main MediaActivity. -->
- <style name="MediaActivityTheme" parent="Theme.Car.Dark.NoActionBar.Drawer" >
- <item name="android:colorPrimary">@android:color/transparent</item>
- <item name="listItemBackgroundColor">@android:color/transparent</item>
- <item name="listItemTitleTextAppearance">@style/TextAppearance.Car.Body1.Light</item>
- <item name="listItemBodyTextAppearance">@style/TextAppearance.Car.Body2.Light</item>
+ <!-- The theme for the Media Center application. -->
+ <style name="Theme.Media" parent="android:Theme.DeviceDefault.NoActionBar" >
+ <item name="textAppearanceGridItem">@android:style/TextAppearance.DeviceDefault.Medium</item>
+ <item name="textAppearanceGridItemSecondary">@android:style/TextAppearance.DeviceDefault.Small</item>
+ <item name="android:splitMotionEvents">false</item>
</style>
</resources>
diff --git a/src/com/android/car/media/AppSelectionFragment.java b/src/com/android/car/media/AppSelectionFragment.java
deleted file mode 100644
index a9a7f59..0000000
--- a/src/com/android/car/media/AppSelectionFragment.java
+++ /dev/null
@@ -1,152 +0,0 @@
-package com.android.car.media;
-
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import androidx.annotation.Nullable;
-import androidx.car.widget.PagedListView;
-import androidx.fragment.app.Fragment;
-import androidx.recyclerview.widget.GridLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.android.car.media.common.MediaSource;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-/**
- * A {@link Fragment} that implements the app selection UI
- */
-public class AppSelectionFragment extends Fragment {
- private AppGridAdapter mGridAdapter;
- @Nullable
- private Callbacks mCallbacks;
-
- private class AppGridAdapter extends RecyclerView.Adapter<AppItemViewHolder> {
- private final LayoutInflater mInflater;
- private List<MediaSource> mMediaSources;
-
- AppGridAdapter() {
- mInflater = LayoutInflater.from(getContext());
- }
-
- /**
- * Triggers a refresh of media sources
- */
- void updateSources(List<MediaSource> mediaSources) {
- mMediaSources = new ArrayList<>(mediaSources);
- notifyDataSetChanged();
- }
-
- @Override
- public AppItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- View view = mInflater.inflate(R.layout.app_selection_item, parent, false);
- return new AppItemViewHolder(view, getContext());
-
- }
-
- @Override
- public void onBindViewHolder(AppItemViewHolder vh, int position) {
- vh.bind(mMediaSources.get(position));
- }
-
- @Override
- public int getItemCount() {
- return mMediaSources.size();
- }
- }
-
- private class AppItemViewHolder extends RecyclerView.ViewHolder {
- private final Context mContext;
- public View mAppItem;
- public ImageView mAppIconView;
- public TextView mAppNameView;
-
- public AppItemViewHolder(View view, Context context) {
- super(view);
- mContext = context;
- mAppItem = view.findViewById(R.id.app_item);
- mAppIconView = mAppItem.findViewById(R.id.app_icon);
- mAppNameView = mAppItem.findViewById(R.id.app_name);
- }
-
- /**
- * Binds a media source to a view
- */
- public void bind(MediaSource mediaSource) {
- // Empty out the view
- mAppItem.setOnClickListener(v -> {
- if (mCallbacks != null) {
- mCallbacks.onMediaSourceSelected(mediaSource);
- }
- });
- mAppIconView.setImageDrawable(mediaSource.getPackageIcon());
- mAppNameView.setText(mediaSource.getName());;
- }
- }
-
- /**
- * Fragment callbacks (implemented by the hosting Activity)
- */
- public interface Callbacks {
- /**
- * Invoked whenever this fragment requires to obtain the list of media source to select
- * from.
- */
- List<MediaSource> getMediaSources();
-
- /**
- * Invoked when the user makes a selection
- */
- void onMediaSourceSelected(MediaSource mediaSource);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, final ViewGroup container,
- Bundle savedInstanceState) {
- View view = inflater.inflate(R.layout.fragment_app_selection, container, false);
- int columnNumber = getResources().getInteger(R.integer.num_app_selector_columns);
- mGridAdapter = new AppGridAdapter();
- PagedListView gridView = view.findViewById(R.id.apps_grid);
-
- GridLayoutManager gridLayoutManager = new GridLayoutManager(getContext(), columnNumber);
- gridView.getRecyclerView().setLayoutManager(gridLayoutManager);
- gridView.setAdapter(mGridAdapter);
- return view;
- }
-
- @Override
- public void onAttach(Context context) {
- super.onAttach(context);
- mCallbacks = (Callbacks) context;
- }
-
- @Override
- public void onDetach() {
- super.onDetach();
- mCallbacks = null;
- }
-
- @Override
- public void onResume() {
- super.onResume();
- refresh();
- }
-
- /**
- * Refreshes the list of media sources.
- */
- public void refresh() {
- if (mCallbacks != null) {
- mGridAdapter.updateSources(mCallbacks.getMediaSources());
- }
- }
-}
diff --git a/src/com/android/car/media/BrowseFragment.java b/src/com/android/car/media/BrowseFragment.java
index 67178fa..5adc766 100644
--- a/src/com/android/car/media/BrowseFragment.java
+++ b/src/com/android/car/media/BrowseFragment.java
@@ -16,112 +16,79 @@
package com.android.car.media;
+import static com.android.car.apps.common.FragmentUtils.checkParent;
+import static com.android.car.apps.common.FragmentUtils.requireParent;
+import static com.android.car.arch.common.LiveDataFunctions.ifThenElse;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
+import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
import android.widget.ImageView;
-import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.fragment.app.Fragment;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
+import com.android.car.apps.common.util.ViewUtils;
+import com.android.car.arch.common.FutureData;
import com.android.car.media.browse.BrowseAdapter;
-import com.android.car.media.browse.ContentForwardStrategy;
import com.android.car.media.common.GridSpacingItemDecoration;
import com.android.car.media.common.MediaItemMetadata;
-import com.android.car.media.common.MediaSource;
-import com.android.car.media.widgets.ViewUtils;
+import com.android.car.media.common.browse.MediaBrowserViewModel;
+import com.android.car.media.common.source.MediaSourceViewModel;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
-import androidx.car.widget.PagedListView;
-
/**
* A {@link Fragment} that implements the content forward browsing experience.
+ *
+ * This can be used to display either search or browse results at the root level. Deeper levels will
+ * be handled the same way between search and browse, using a backstack to return to the root.
*/
public class BrowseFragment extends Fragment {
private static final String TAG = "BrowseFragment";
private static final String TOP_MEDIA_ITEM_KEY = "top_media_item";
- private static final String MEDIA_SOURCE_PACKAGE_NAME_KEY = "media_source";
+ private static final String SEARCH_KEY = "search_config";
private static final String BROWSE_STACK_KEY = "browse_stack";
- private PagedListView mBrowseList;
- private ProgressBar mProgressBar;
+ private RecyclerView mBrowseList;
private ImageView mErrorIcon;
- private TextView mErrorMessage;
- private MediaSource mMediaSource;
+ private TextView mMessage;
private BrowseAdapter mBrowseAdapter;
- private String mMediaSourcePackageName;
private MediaItemMetadata mTopMediaItem;
- private Callbacks mCallbacks;
+ private String mSearchQuery;
private int mFadeDuration;
- private int mProgressBarDelay;
+ private int mLoadingIndicatorDelay;
+ private boolean mIsSearchFragment;
+ private boolean mPlaybackControlsVisible = false;
+ // todo(b/130760002): Create new browse fragments at deeper levels.
+ private MutableLiveData<Boolean> mShowSearchResults = new MutableLiveData<>();
private Handler mHandler = new Handler();
private Stack<MediaItemMetadata> mBrowseStack = new Stack<>();
- private MediaSource.Observer mBrowseObserver = new MediaSource.Observer() {
- @Override
- protected void onBrowseConnected(boolean success) {
- BrowseFragment.this.onBrowseConnected(success);
- }
-
- @Override
- protected void onBrowseDisconnected() {
- BrowseFragment.this.onBrowseDisconnected();
- }
- };
+ private MediaBrowserViewModel.WithMutableBrowseId mMediaBrowserViewModel;
private BrowseAdapter.Observer mBrowseAdapterObserver = new BrowseAdapter.Observer() {
- @Override
- protected void onDirty() {
- switch (mBrowseAdapter.getState()) {
- case LOADING:
- case IDLE:
- // Still loading... nothing to do.
- break;
- case LOADED:
- stopLoadingIndicator();
- mBrowseAdapter.update();
- if (mBrowseAdapter.getItemCount() > 0) {
- ViewUtils.showViewAnimated(mBrowseList, mFadeDuration);
- ViewUtils.hideViewAnimated(mErrorIcon, mFadeDuration);
- ViewUtils.hideViewAnimated(mErrorMessage, mFadeDuration);
- } else {
- mErrorMessage.setText(R.string.nothing_to_play);
- ViewUtils.hideViewAnimated(mBrowseList, mFadeDuration);
- ViewUtils.hideViewAnimated(mErrorIcon, mFadeDuration);
- ViewUtils.showViewAnimated(mErrorMessage, mFadeDuration);
- }
- break;
- case ERROR:
- stopLoadingIndicator();
- mErrorMessage.setText(R.string.unknown_error);
- ViewUtils.hideViewAnimated(mBrowseList, mFadeDuration);
- ViewUtils.showViewAnimated(mErrorMessage, mFadeDuration);
- ViewUtils.showViewAnimated(mErrorIcon, mFadeDuration);
- break;
- }
- }
@Override
protected void onPlayableItemClicked(MediaItemMetadata item) {
- mCallbacks.onPlayableItemClicked(mMediaSource, item);
- }
-
- @Override
- protected void onBrowseableItemClicked(MediaItemMetadata item) {
- navigateInto(item);
+ hideKeyboard();
+ getParent().onPlayableItemClicked(item);
}
@Override
- protected void onMoreButtonClicked(MediaItemMetadata item) {
+ protected void onBrowsableItemClicked(MediaItemMetadata item) {
navigateInto(item);
}
};
@@ -131,11 +98,6 @@ public class BrowseFragment extends Fragment {
*/
public interface Callbacks {
/**
- * @return a {@link MediaSource} corresponding to the given package name
- */
- MediaSource getMediaSource(String packageName);
-
- /**
* Method invoked when the back stack changes (for example, when the user moves up or down
* the media tree)
*/
@@ -144,55 +106,95 @@ public class BrowseFragment extends Fragment {
/**
* Method invoked when the user clicks on a playable item
*
- * @param mediaSource {@link MediaSource} the playable item belongs to
* @param item item to be played.
*/
- void onPlayableItemClicked(MediaSource mediaSource, MediaItemMetadata item);
+ void onPlayableItemClicked(MediaItemMetadata item);
}
/**
- * Moves the user one level up in the browse tree, if possible.
+ * Moves the user one level up in the browse tree. Returns whether that was possible.
*/
- public void navigateBack() {
- mBrowseStack.pop();
- if (mBrowseAdapter != null) {
- mBrowseAdapter.setParentMediaItemId(getCurrentMediaItem());
+ boolean navigateBack() {
+ boolean result = false;
+ if (!mBrowseStack.empty()) {
+ mBrowseStack.pop();
+ mMediaBrowserViewModel.search(mSearchQuery);
+ mMediaBrowserViewModel.setCurrentBrowseId(getCurrentMediaItemId());
+ getParent().onBackStackChanged();
+ adjustBrowseTopPadding();
+ result = true;
}
- if (mCallbacks != null) {
- mCallbacks.onBackStackChanged();
+ if (mBrowseStack.isEmpty()) {
+ mShowSearchResults.setValue(mIsSearchFragment);
}
+ return result;
+ }
+
+ @NonNull
+ private Callbacks getParent() {
+ return requireParent(this, Callbacks.class);
}
/**
- * @return whether the user is in a level other than the top.
+ * @return whether the user is at the top of the browsing stack.
*/
- public boolean isBackEnabled() {
- return !mBrowseStack.isEmpty();
+ public boolean isAtTopStack() {
+ return mBrowseStack.isEmpty();
}
/**
- * Creates a new instance of this fragment.
+ * Creates a new instance of this fragment. The root browse id will be the one provided to this
+ * method.
*
- * @param mediaSource media source being displayed
* @param item media tree node to display on this fragment.
* @return a fully initialized {@link BrowseFragment}
*/
- public static BrowseFragment newInstance(MediaSource mediaSource, MediaItemMetadata item) {
+ public static BrowseFragment newInstance(MediaItemMetadata item) {
BrowseFragment fragment = new BrowseFragment();
Bundle args = new Bundle();
args.putParcelable(TOP_MEDIA_ITEM_KEY, item);
- args.putString(MEDIA_SOURCE_PACKAGE_NAME_KEY, mediaSource.getPackageName());
fragment.setArguments(args);
return fragment;
}
+ /**
+ * Creates a new instance of this fragment, meant to display search results. The root browse
+ * screen will be the search results for the provided query.
+ *
+ * @return a fully initialized {@link BrowseFragment}
+ */
+ public static BrowseFragment newSearchInstance() {
+ BrowseFragment fragment = new BrowseFragment();
+ Bundle args = new Bundle();
+ args.putBoolean(SEARCH_KEY, true);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ public void updateSearchQuery(@Nullable String query) {
+ mSearchQuery = query;
+ mMediaBrowserViewModel.search(query);
+ }
+
+ /**
+ * Clears search state from this fragment, removes any UI elements from previous results.
+ */
+ public void resetSearchState() {
+ updateSearchQuery(null);
+ mBrowseAdapter.submitItems(null, null);
+ stopLoadingIndicator();
+ ViewUtils.hideViewAnimated(mErrorIcon, mFadeDuration);
+ ViewUtils.hideViewAnimated(mMessage, mFadeDuration);
+ }
+
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle arguments = getArguments();
if (arguments != null) {
mTopMediaItem = arguments.getParcelable(TOP_MEDIA_ITEM_KEY);
- mMediaSourcePackageName = arguments.getString(MEDIA_SOURCE_PACKAGE_NAME_KEY);
+ mIsSearchFragment = arguments.getBoolean(SEARCH_KEY, false);
+ mShowSearchResults.setValue(mIsSearchFragment);
}
if (savedInstanceState != null) {
List<MediaItemMetadata> savedStack =
@@ -202,86 +204,120 @@ public class BrowseFragment extends Fragment {
mBrowseStack.addAll(savedStack);
}
}
+
+ // Get the MediaBrowserViewModel tied to the lifecycle of this fragment, but using the
+ // MediaSourceViewModel of the activity. This means the media source is consistent across
+ // all fragments, but the fragment contents themselves will vary
+ // (e.g. between different browse tabs, search)
+ mMediaBrowserViewModel = MediaBrowserViewModel.Factory.getInstanceWithMediaBrowser(
+ ViewModelProviders.of(this),
+ MediaSourceViewModel.get(
+ requireActivity().getApplication()).getConnectedMediaBrowser());
+
+ MediaActivity.ViewModel viewModel = ViewModelProviders.of(requireActivity()).get(
+ MediaActivity.ViewModel.class);
+ viewModel.getMiniControlsVisible().observe(this, (visible) -> {
+ mPlaybackControlsVisible = visible;
+ adjustBrowseTopPadding();
+ });
+
}
@Override
- public View onCreateView(LayoutInflater inflater, final ViewGroup container,
+ public View onCreateView(@NonNull LayoutInflater inflater, final ViewGroup container,
Bundle savedInstanceState) {
- View view = inflater.inflate(R.layout.fragment_browse, container, false);
- mProgressBar = view.findViewById(R.id.loading_spinner);
- mProgressBarDelay = getContext().getResources()
+ int viewId = mIsSearchFragment ? R.layout.fragment_search : R.layout.fragment_browse;
+ View view = inflater.inflate(viewId, container, false);
+ mLoadingIndicatorDelay = view.getContext().getResources()
.getInteger(R.integer.progress_indicator_delay);
mBrowseList = view.findViewById(R.id.browse_list);
mErrorIcon = view.findViewById(R.id.error_icon);
- mErrorMessage = view.findViewById(R.id.error_message);
- mFadeDuration = getContext().getResources().getInteger(
+ mMessage = view.findViewById(R.id.error_message);
+ mFadeDuration = view.getContext().getResources().getInteger(
R.integer.new_album_art_fade_in_duration);
- int numColumns = getContext().getResources().getInteger(R.integer.num_browse_columns);
+ int numColumns = view.getContext().getResources().getInteger(R.integer.num_browse_columns);
GridLayoutManager gridLayoutManager = new GridLayoutManager(getContext(), numColumns);
- RecyclerView recyclerView = mBrowseList.getRecyclerView();
- recyclerView.setVerticalFadingEdgeEnabled(true);
- recyclerView.setFadingEdgeLength(getResources()
- .getDimensionPixelSize(R.dimen.car_padding_5));
- recyclerView.setLayoutManager(gridLayoutManager);
- recyclerView.addItemDecoration(new GridSpacingItemDecoration(
- getResources().getDimensionPixelSize(R.dimen.car_padding_4),
- getResources().getDimensionPixelSize(R.dimen.car_keyline_1),
- getResources().getDimensionPixelSize(R.dimen.car_keyline_1)
+
+ mBrowseList.setLayoutManager(gridLayoutManager);
+ mBrowseList.addItemDecoration(new GridSpacingItemDecoration(
+ getResources().getDimensionPixelSize(R.dimen.grid_item_spacing),
+ getResources().getDimensionPixelSize(R.dimen.grid_item_margin_x),
+ getResources().getDimensionPixelSize(R.dimen.grid_item_margin_x)
));
+
+ mBrowseAdapter = new BrowseAdapter(mBrowseList.getContext());
+ mBrowseList.setAdapter(mBrowseAdapter);
+ mBrowseAdapter.registerObserver(mBrowseAdapterObserver);
+
+ if (savedInstanceState == null) {
+ mMediaBrowserViewModel.search(mSearchQuery);
+ mMediaBrowserViewModel.setCurrentBrowseId(getCurrentMediaItemId());
+ }
+ mMediaBrowserViewModel.rootBrowsableHint().observe(this, hint ->
+ mBrowseAdapter.setRootBrowsableViewType(hint));
+ mMediaBrowserViewModel.rootPlayableHint().observe(this, hint ->
+ mBrowseAdapter.setRootPlayableViewType(hint));
+ LiveData<FutureData<List<MediaItemMetadata>>> mediaItems = ifThenElse(mShowSearchResults,
+ mMediaBrowserViewModel.getSearchedMediaItems(),
+ mMediaBrowserViewModel.getBrowsedMediaItems());
+
+ mediaItems.observe(getViewLifecycleOwner(), futureData ->
+ {
+ // Prevent showing loading spinner or any error messages if search is uninitialized
+ if (mIsSearchFragment && TextUtils.isEmpty(mSearchQuery)) {
+ return;
+ }
+ boolean isLoading = futureData.isLoading();
+ if (isLoading) {
+ ViewUtils.hideViewAnimated(mBrowseList, mFadeDuration);
+ startLoadingIndicator();
+ return;
+ }
+ stopLoadingIndicator();
+ List<MediaItemMetadata> items = futureData.getData();
+ mBrowseAdapter.submitItems(getCurrentMediaItem(), items);
+ if (items == null) {
+ mMessage.setText(R.string.unknown_error);
+ ViewUtils.hideViewAnimated(mBrowseList, mFadeDuration);
+ ViewUtils.showViewAnimated(mMessage, mFadeDuration);
+ ViewUtils.showViewAnimated(mErrorIcon, mFadeDuration);
+ } else if (items.isEmpty()) {
+ mMessage.setText(R.string.nothing_to_play);
+ ViewUtils.hideViewAnimated(mBrowseList, mFadeDuration);
+ ViewUtils.hideViewAnimated(mErrorIcon, mFadeDuration);
+ ViewUtils.showViewAnimated(mMessage, mFadeDuration);
+ } else {
+ ViewUtils.showViewAnimated(mBrowseList, mFadeDuration);
+ ViewUtils.hideViewAnimated(mErrorIcon, mFadeDuration);
+ ViewUtils.hideViewAnimated(mMessage, mFadeDuration);
+ }
+ });
return view;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
- mCallbacks = (Callbacks) context;
- }
-
- @Override
- public void onDetach() {
- super.onDetach();
- mCallbacks = null;
+ checkParent(this, Callbacks.class);
}
- @Override
- public void onStart() {
- super.onStart();
- startLoadingIndicator();
- mMediaSource = mCallbacks.getMediaSource(mMediaSourcePackageName);
- if (mMediaSource != null) {
- mMediaSource.subscribe(mBrowseObserver);
- }
- }
-
- private Runnable mProgressIndicatorRunnable = new Runnable() {
+ private Runnable mLoadingIndicatorRunnable = new Runnable() {
@Override
public void run() {
- ViewUtils.showViewAnimated(mProgressBar, mFadeDuration);
+ mMessage.setText(R.string.browser_loading);
+ ViewUtils.showViewAnimated(mMessage, mFadeDuration);
}
};
private void startLoadingIndicator() {
// Display the indicator after a certain time, to avoid flashing the indicator constantly,
// even when performance is acceptable.
- mHandler.postDelayed(mProgressIndicatorRunnable, mProgressBarDelay);
+ mHandler.postDelayed(mLoadingIndicatorRunnable, mLoadingIndicatorDelay);
}
private void stopLoadingIndicator() {
- mHandler.removeCallbacks(mProgressIndicatorRunnable);
- ViewUtils.hideViewAnimated(mProgressBar, mFadeDuration);
- }
-
- @Override
- public void onStop() {
- super.onStop();
- stopLoadingIndicator();
- if (mMediaSource != null) {
- mMediaSource.unsubscribe(mBrowseObserver);
- }
- if (mBrowseAdapter != null) {
- mBrowseAdapter.stop();
- mBrowseAdapter = null;
- }
+ mHandler.removeCallbacks(mLoadingIndicatorRunnable);
+ ViewUtils.hideViewAnimated(mMessage, mFadeDuration);
}
@Override
@@ -291,48 +327,53 @@ public class BrowseFragment extends Fragment {
outState.putParcelableArrayList(BROWSE_STACK_KEY, stack);
}
- private void onBrowseConnected(boolean success) {
- if (mBrowseAdapter != null) {
- mBrowseAdapter.stop();
- mBrowseAdapter = null;
- }
- if (!success) {
- ViewUtils.hideViewAnimated(mBrowseList, mFadeDuration);
- stopLoadingIndicator();
- mErrorMessage.setText(R.string.cannot_connect_to_app);
- ViewUtils.showViewAnimated(mErrorIcon, mFadeDuration);
- ViewUtils.showViewAnimated(mErrorMessage, mFadeDuration);
- return;
- }
- mBrowseAdapter = new BrowseAdapter(getContext(), mMediaSource, getCurrentMediaItem(),
- ContentForwardStrategy.DEFAULT_STRATEGY);
- mBrowseList.setAdapter(mBrowseAdapter);
- mBrowseList.setDividerVisibilityManager(mBrowseAdapter);
- mBrowseAdapter.registerObserver(mBrowseAdapterObserver);
- mBrowseAdapter.start();
- }
-
- private void onBrowseDisconnected() {
- if (mBrowseAdapter != null) {
- mBrowseAdapter.stop();
- mBrowseAdapter = null;
- }
- }
-
private void navigateInto(MediaItemMetadata item) {
+ hideKeyboard();
mBrowseStack.push(item);
- mBrowseAdapter.setParentMediaItemId(item);
- mCallbacks.onBackStackChanged();
+ mShowSearchResults.setValue(false);
+ mMediaBrowserViewModel.setCurrentBrowseId(item.getId());
+ getParent().onBackStackChanged();
+ adjustBrowseTopPadding();
}
/**
* @return the current item being displayed
*/
- public MediaItemMetadata getCurrentMediaItem() {
+ @Nullable
+ MediaItemMetadata getCurrentMediaItem() {
if (mBrowseStack.isEmpty()) {
return mTopMediaItem;
} else {
return mBrowseStack.lastElement();
}
}
+
+ @Nullable
+ private String getCurrentMediaItemId() {
+ MediaItemMetadata currentItem = getCurrentMediaItem();
+ return currentItem != null ? currentItem.getId() : null;
+ }
+
+ private void adjustBrowseTopPadding() {
+ if(mBrowseList == null) {
+ return;
+ }
+
+ int topPadding = isAtTopStack()
+ ? getResources().getDimensionPixelOffset(R.dimen.browse_fragment_top_padding)
+ : getResources().getDimensionPixelOffset(
+ R.dimen.browse_fragment_top_padding_stacked);
+ int bottomPadding = mPlaybackControlsVisible
+ ? getResources().getDimensionPixelOffset(R.dimen.browse_fragment_bottom_padding)
+ : 0;
+
+ mBrowseList.setPadding(mBrowseList.getPaddingLeft(), topPadding,
+ mBrowseList.getPaddingRight(), bottomPadding);
+ }
+
+ private void hideKeyboard() {
+ InputMethodManager in =
+ (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ in.hideSoftInputFromWindow(getView().getWindowToken(), 0);
+ }
}
diff --git a/src/com/android/car/media/EmptyFragment.java b/src/com/android/car/media/EmptyFragment.java
index 358667f..b810f39 100644
--- a/src/com/android/car/media/EmptyFragment.java
+++ b/src/com/android/car/media/EmptyFragment.java
@@ -7,45 +7,47 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
-import android.widget.ProgressBar;
import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
-import com.android.car.media.common.MediaSource;
-import com.android.car.media.widgets.ViewUtils;
+import com.android.car.apps.common.util.ViewUtils;
+import com.android.car.media.common.browse.MediaBrowserViewModel;
+import com.android.car.media.common.source.MediaSource;
/**
* Empty fragment to show while we are loading content
*/
public class EmptyFragment extends Fragment {
- private ProgressBar mProgressBar;
+ private static final String TAG = "EmptyFragment";
+
private ImageView mErrorIcon;
- private TextView mErrorMessage;
+ private TextView mMessage;
- private int mProgressBarDelay;
+ private int mLoadingIndicatorDelay;
private Handler mHandler = new Handler();
private int mFadeDuration;
- private MediaActivity.BrowseState mState = MediaActivity.BrowseState.EMPTY;
- private MediaSource mMediaSource;
private Runnable mProgressIndicatorRunnable = new Runnable() {
@Override
public void run() {
- ViewUtils.showViewAnimated(mProgressBar, mFadeDuration);
+ mMessage.setText(requireContext().getString(R.string.browser_loading));
+ ViewUtils.showViewAnimated(mMessage, mFadeDuration);
}
};
+
@Override
- public View onCreateView(LayoutInflater inflater, final ViewGroup container,
+ public View onCreateView(@NonNull LayoutInflater inflater, final ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_empty, container, false);
- mProgressBar = view.findViewById(R.id.loading_spinner);
- mProgressBarDelay = getContext().getResources()
+ mLoadingIndicatorDelay = requireContext().getResources()
.getInteger(R.integer.progress_indicator_delay);
- mFadeDuration = getContext().getResources().getInteger(
+ mFadeDuration = requireContext().getResources().getInteger(
R.integer.new_album_art_fade_in_duration);
mErrorIcon = view.findViewById(R.id.error_icon);
- mErrorMessage = view.findViewById(R.id.error_message);
- update();
+ mMessage = view.findViewById(R.id.error_message);
+
return view;
}
@@ -58,46 +60,49 @@ public class EmptyFragment extends Fragment {
/**
* Updates the state of this fragment
*
- * @param state browsing state to display
+ * @param state browsing state to display
* @param mediaSource media source currently being browsed
*/
- public void setState(MediaActivity.BrowseState state, MediaSource mediaSource) {
+ void setState(@NonNull MediaBrowserViewModel.BrowseState state,
+ @Nullable MediaSource mediaSource) {
mHandler.removeCallbacks(mProgressIndicatorRunnable);
- mMediaSource = mediaSource;
- mState = state;
if (this.getView() != null) {
- update();
+ update(state, mediaSource);
}
}
- private void update() {
- switch (mState) {
+ private void update(@NonNull MediaBrowserViewModel.BrowseState state,
+ @Nullable MediaSource mediaSource) {
+ switch (state) {
case LOADING:
// Display the indicator after a certain time, to avoid flashing the indicator
// constantly, even when performance is acceptable.
- mHandler.postDelayed(mProgressIndicatorRunnable, mProgressBarDelay);
+ mHandler.postDelayed(mProgressIndicatorRunnable, mLoadingIndicatorDelay);
mErrorIcon.setVisibility(View.GONE);
- mErrorMessage.setVisibility(View.GONE);
+ mMessage.setVisibility(View.GONE);
break;
case ERROR:
- mProgressBar.setVisibility(View.GONE);
mErrorIcon.setVisibility(View.VISIBLE);
- mErrorMessage.setVisibility(View.VISIBLE);
- mErrorMessage.setText(getContext().getString(
+ mMessage.setVisibility(View.VISIBLE);
+ mMessage.setText(requireContext().getString(
R.string.cannot_connect_to_app,
- mMediaSource != null
- ? mMediaSource.getName()
- : getContext().getString(R.string.unknown_media_provider_name)));
+ mediaSource != null
+ ? mediaSource.getName()
+ : requireContext().getString(
+ R.string.unknown_media_provider_name)));
break;
case EMPTY:
- mProgressBar.setVisibility(View.GONE);
mErrorIcon.setVisibility(View.GONE);
- mErrorMessage.setVisibility(View.VISIBLE);
- mErrorMessage.setText(getContext().getString(R.string.nothing_to_play));
+ mMessage.setVisibility(View.VISIBLE);
+ mMessage.setText(requireContext().getString(R.string.nothing_to_play));
+ break;
+ case LOADED:
+ Log.d(TAG, "Updated with LOADED state, ignoring.");
+ // Do nothing, this fragment is about to be removed
break;
default:
// Fail fast on any other state.
- throw new IllegalStateException("Invalid state for this fragment: " + mState);
+ throw new IllegalStateException("Invalid state for this fragment: " + state);
}
}
}
diff --git a/src/com/android/car/media/ErrorFragment.java b/src/com/android/car/media/ErrorFragment.java
new file mode 100644
index 0000000..6e38e98
--- /dev/null
+++ b/src/com/android/car/media/ErrorFragment.java
@@ -0,0 +1,90 @@
+package com.android.car.media;
+
+import android.app.PendingIntent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+
+import com.android.car.apps.common.UxrTextView;
+
+/**
+ * A {@link Fragment} that displays the playback state error.
+ */
+public class ErrorFragment extends Fragment {
+ private final String TAG = "ErrorFragment";
+
+ private static final String ERROR_RESOLUTION_ACTION_MESSAGE = "ERROR_RESOLUTION_ACTION_MESSAGE";
+ private static final String ERROR_RESOLUTION_ACTION_LABEL = "ERROR_RESOLUTION_ACTION_LABEL";
+ private static final String ERROR_RESOLUTION_ACTION_INTENT = "ERROR_RESOLUTION_ACTION_INTENT";
+
+ // mErrorMessageView is defined explicitly as a UxrTextView instead of a TextView to
+ // provide clarity as it may be misleading to assume that mErrorMessageView extends all TextView
+ // methods. In addition, it increases discoverability of runtime issues that may occur.
+ private UxrTextView mErrorMessageView;
+ private Button mErrorButton;
+
+ private String mErrorMessageStr;
+ private String mErrorLabel;
+ private PendingIntent mPendingIntent;
+
+ public static ErrorFragment newInstance(String message, String label, PendingIntent intent) {
+ ErrorFragment fragment = new ErrorFragment();
+
+ Bundle args = new Bundle();
+ args.putString(ERROR_RESOLUTION_ACTION_MESSAGE, message);
+ args.putString(ERROR_RESOLUTION_ACTION_LABEL, label);
+ args.putParcelable(ERROR_RESOLUTION_ACTION_INTENT, intent);
+ fragment.setArguments(args);
+
+ return fragment;
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, final ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_error, container, false);
+
+ mErrorMessageView = view.findViewById(R.id.error_message);
+ mErrorButton = view.findViewById(R.id.error_button);
+
+ Bundle args = getArguments();
+ if (args != null) {
+ mErrorMessageStr = args.getString(ERROR_RESOLUTION_ACTION_MESSAGE);
+ mErrorLabel = args.getString(ERROR_RESOLUTION_ACTION_LABEL);
+ mPendingIntent = args.getParcelable(ERROR_RESOLUTION_ACTION_INTENT);
+ }
+
+ if (mErrorMessageStr == null) {
+ Log.e(TAG, "ErrorFragment does not have an error message");
+ return view;
+ }
+
+ mErrorMessageView.setText(mErrorMessageStr);
+
+ // Only an error message is required. Fragments without a provided message and label
+ // have these elements omitted.
+ if (mErrorLabel != null && mPendingIntent != null) {
+ mErrorButton.setText(mErrorLabel);
+ mErrorButton.setOnClickListener(v -> {
+ try {
+ mPendingIntent.send();
+ } catch (PendingIntent.CanceledException e) {
+ if (Log.isLoggable(TAG, Log.ERROR)) {
+ Log.e(TAG, "Pending intent canceled");
+ }
+ }
+ });
+ mErrorButton.setVisibility(View.VISIBLE);
+ } else {
+ mErrorButton.setVisibility(View.GONE);
+ }
+
+ return view;
+ }
+}
diff --git a/src/com/android/car/media/MediaActivity.java b/src/com/android/car/media/MediaActivity.java
index 0053bb1..2cb96d7 100644
--- a/src/com/android/car/media/MediaActivity.java
+++ b/src/com/android/car/media/MediaActivity.java
@@ -15,192 +15,153 @@
*/
package com.android.car.media;
+import android.annotation.SuppressLint;
+import android.app.AlertDialog;
+import android.app.Application;
+import android.app.PendingIntent;
import android.car.Car;
-import android.content.ComponentName;
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
-import android.content.SharedPreferences;
-import android.graphics.Bitmap;
-import android.media.session.MediaController;
+import android.content.pm.ResolveInfo;
import android.os.Bundle;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.text.TextUtils;
import android.transition.Fade;
import android.util.Log;
-import android.util.TypedValue;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
import android.view.View;
+import android.view.ViewConfiguration;
import android.view.ViewGroup;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-import java.util.stream.Collectors;
+import android.widget.Toast;
import androidx.annotation.NonNull;
-import androidx.drawerlayout.widget.DrawerLayout;
+import androidx.annotation.Nullable;
+import androidx.core.view.GestureDetectorCompat;
import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentManager;
-import androidx.car.drawer.CarDrawerActivity;
-import androidx.car.drawer.CarDrawerAdapter;
-
-import com.android.car.media.common.ActiveMediaSourceManager;
-import com.android.car.media.common.CrossfadeImageView;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModelProviders;
+
+import com.android.car.apps.common.CarUxRestrictionsUtil;
+import com.android.car.apps.common.util.ViewUtils;
+import com.android.car.media.common.AppSelectionFragment;
+import com.android.car.media.common.MediaAppSelectorWidget;
+import com.android.car.media.common.MediaConstants;
import com.android.car.media.common.MediaItemMetadata;
-import com.android.car.media.common.MediaSource;
-import com.android.car.media.common.MediaSourcesManager;
-import com.android.car.media.common.PlaybackControls;
-import com.android.car.media.common.PlaybackModel;
-import com.android.car.media.drawer.MediaDrawerController;
+import com.android.car.media.common.MinimizedPlaybackControlBar;
+import com.android.car.media.common.browse.MediaBrowserViewModel;
+import com.android.car.media.common.playback.PlaybackViewModel;
+import com.android.car.media.common.source.MediaSource;
+import com.android.car.media.common.source.MediaSourceViewModel;
import com.android.car.media.widgets.AppBarView;
-import com.android.car.media.widgets.MetadataView;
-import com.android.car.media.widgets.ViewUtils;
+import com.android.car.media.widgets.SearchBar;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
/**
* This activity controls the UI of media. It also updates the connection status for the media app
- * by broadcast. Drawer menu is controlled by {@link MediaDrawerController}.
+ * by broadcast.
*/
-public class MediaActivity extends CarDrawerActivity implements BrowseFragment.Callbacks,
- AppSelectionFragment.Callbacks, PlaybackFragment.Callbacks {
+public class MediaActivity extends FragmentActivity implements BrowseFragment.Callbacks,
+ AppBarView.AppBarProvider {
private static final String TAG = "MediaActivity";
- /** Intent extra specifying the package with the MediaBrowser */
- public static final String KEY_MEDIA_PACKAGE = "media_package";
- /** Shared preferences files */
- public static final String SHARED_PREF = "com.android.car.media";
- /** Shared preference containing the last controlled source */
- public static final String LAST_MEDIA_SOURCE_SHARED_PREF_KEY = "last_media_source";
-
/** Configuration (controlled from resources) */
- private boolean mContentForwardBrowseEnabled;
- private float mBackgroundBlurRadius;
- private float mBackgroundBlurScale;
private int mFadeDuration;
/** Models */
- private MediaDrawerController mDrawerController;
- private ActiveMediaSourceManager mActiveMediaSourceManager;
- private MediaSource mMediaSource;
- private PlaybackModel mPlaybackModel;
- private MediaSourcesManager mMediaSourcesManager;
- private SharedPreferences mSharedPreferences;
+ private PlaybackViewModel.PlaybackController mPlaybackController;
/** Layout views */
+ private View mRootView;
private AppBarView mAppBarView;
- private CrossfadeImageView mAlbumBackground;
private PlaybackFragment mPlaybackFragment;
+ private BrowseFragment mSearchFragment;
+ private BrowseFragment mBrowseFragment;
private AppSelectionFragment mAppSelectionFragment;
- private PlaybackControls mPlaybackControls;
- private MetadataView mMetadataView;
- private ViewGroup mBrowseControlsContainer;
+ private ViewGroup mMiniPlaybackControls;
private EmptyFragment mEmptyFragment;
private ViewGroup mBrowseContainer;
private ViewGroup mPlaybackContainer;
+ private ViewGroup mErrorContainer;
+ private ErrorFragment mErrorFragment;
+ private ViewGroup mSearchContainer;
- /** Current state */
- private Fragment mCurrentFragment;
- private Mode mMode = Mode.BROWSING;
- private boolean mIsAppSelectorOpen;
- private MediaItemMetadata mCurrentMetadata;
-
- private MediaSource.Observer mMediaSourceObserver = new MediaSource.Observer() {
- @Override
- protected void onBrowseConnected(boolean success) {
- MediaActivity.this.onBrowseConnected(success);
- }
+ private Toast mToast;
- @Override
- protected void onBrowseDisconnected() {
- MediaActivity.this.onBrowseConnected(false);
- }
- };
- private PlaybackModel.PlaybackObserver mPlaybackObserver =
- new PlaybackModel.PlaybackObserver() {
- @Override
- protected void onSourceChanged() {
- updateMetadata();
- }
+ /** Current state */
+ private Mode mMode;
+ private Intent mCurrentSourcePreferences;
+ private boolean mCanShowMiniPlaybackControls;
+ private boolean mIsBrowseTreeReady;
+ private Integer mCurrentPlaybackState;
+ private List<MediaItemMetadata> mTopItems;
+
+ private CarUxRestrictionsUtil mCarUxRestrictionsUtil;
+ private CarUxRestrictions mActiveCarUxRestrictions;
+ @CarUxRestrictions.CarUxRestrictionsInfo
+ private int mRestrictions;
+ private final CarUxRestrictionsUtil.OnUxRestrictionsChangedListener mListener =
+ (carUxRestrictions) -> mActiveCarUxRestrictions = carUxRestrictions;
- @Override
- public void onMetadataChanged() {
- mCurrentMetadata = null;
- updateMetadata();
- }
- };
- private ActiveMediaSourceManager.Observer mActiveSourceObserver = () -> {
- // If the active media source changes and it is the one currently being browsed, then
- // we should capture the controller.
- MediaController controller = mActiveMediaSourceManager.getMediaController();
- if (mPlaybackModel.getMediaController() == null
- && mMediaSource != null
- && controller != null
- && Objects.equals(controller.getPackageName(), mMediaSource.getPackageName())) {
- mPlaybackModel.setMediaController(controller);
- }
- };
- private MediaSource.ItemsSubscription mItemsSubscription =
- new MediaSource.ItemsSubscription() {
- @Override
- public void onChildrenLoaded(MediaSource mediaSource, String parentId,
- List<MediaItemMetadata> items) {
- if (mediaSource == mMediaSource) {
- updateTabs(items);
- } else {
- Log.w(TAG, "Received items for a wrong source: " +
- mediaSource.getPackageName());
- }
- }
- };
private AppBarView.AppBarListener mAppBarListener = new AppBarView.AppBarListener() {
@Override
public void onTabSelected(MediaItemMetadata item) {
- updateBrowseFragment(BrowseState.LOADED, item);
- switchToMode(Mode.BROWSING);
+ showTopItem(item);
+ changeMode(Mode.BROWSING);
}
@Override
public void onBack() {
- if (mCurrentFragment != null && mCurrentFragment instanceof BrowseFragment) {
- BrowseFragment fragment = (BrowseFragment) mCurrentFragment;
- fragment.navigateBack();
+ BrowseFragment fragment = getCurrentBrowseFragment();
+ if (fragment != null) {
+ boolean success = fragment.navigateBack();
+ if (!success && (fragment == mSearchFragment)) {
+ changeMode(Mode.BROWSING);
+ }
}
}
@Override
- public void onCollapse() {
- switchToMode(Mode.BROWSING);
- }
-
- @Override
- public void onAppSelection() {
- Log.d(TAG, "onAppSelection clicked");
- if (mIsAppSelectorOpen) {
- closeAppSelector();
- } else {
- openAppSelector();
+ public void onSettingsSelection() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onSettingsSelection");
+ }
+ try {
+ if (mCurrentSourcePreferences != null) {
+ startActivity(mCurrentSourcePreferences);
+ }
+ } catch (ActivityNotFoundException e) {
+ if (Log.isLoggable(TAG, Log.ERROR)) {
+ Log.e(TAG, "onSettingsSelection " + e);
+ }
}
- }
- };
- private MediaSourcesManager.Observer mMediaSourcesManagerObserver = () -> {
- mAppBarView.setAppSelection(!mMediaSourcesManager.getMediaSources().isEmpty());
- mAppSelectionFragment.refresh();
- };
- private DrawerLayout.DrawerListener mDrawerListener = new DrawerLayout.DrawerListener() {
- @Override
- public void onDrawerSlide(@androidx.annotation.NonNull View view, float v) {
- }
-
- @Override
- public void onDrawerOpened(@androidx.annotation.NonNull View view) {
- closeAppSelector();
}
@Override
- public void onDrawerClosed(@androidx.annotation.NonNull View view) {
+ public void onSearchSelection() {
+ changeMode(Mode.SEARCHING);
}
@Override
- public void onDrawerStateChanged(int i) {
+ public void onSearch(String query) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onSearch: " + query);
+ }
+ mSearchFragment.updateSearchQuery(query);
}
};
+ private PlaybackFragment.PlaybackFragmentListener mPlaybackFragmentListener =
+ () -> changeMode(Mode.BROWSING);
+
/**
* Possible modes of the application UI
*/
@@ -208,485 +169,520 @@ public class MediaActivity extends CarDrawerActivity implements BrowseFragment.C
/** The user is browsing a media source */
BROWSING,
/** The user is interacting with the full screen playback UI */
- PLAYBACK
- }
-
- /**
- * Possible states of the application UI
- */
- public enum BrowseState {
- /** There is no content to show */
- EMPTY,
- /** We are still in the process of obtaining data */
- LOADING,
- /** Data has been loaded */
- LOADED,
- /** The content can't be shown due an error */
- ERROR
+ PLAYBACK,
+ /** The user is searching within a media source */
+ SEARCHING,
+ /** There's no browse tree and playback doesn't work.*/
+ FATAL_ERROR
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ setContentView(R.layout.media_activity);
- setMainContent(R.layout.media_activity);
- setToolbarElevation(0f);
-
- mContentForwardBrowseEnabled = getResources()
- .getBoolean(R.bool.forward_content_browse_enabled);
- mDrawerController = new MediaDrawerController(this, getDrawerController());
- getDrawerController().setRootAdapter(getRootAdapter());
- getDrawerController().addDrawerListener(mDrawerListener);
- if (mContentForwardBrowseEnabled) {
- getSupportActionBar().hide();
+ MediaSourceViewModel mediaSourceViewModel = getMediaSourceViewModel();
+ PlaybackViewModel playbackViewModel = getPlaybackViewModel();
+ ViewModel localViewModel = getInnerViewModel();
+ // We can't rely on savedInstanceState to determine whether the model has been initialized
+ // as on a config change savedInstanceState != null and the model is initialized, but if
+ // the app was killed by the system then savedInstanceState != null and the model is NOT
+ // initialized...
+ if (localViewModel.needsInitialization()) {
+ localViewModel.init(playbackViewModel);
}
+ mMode = localViewModel.getSavedMode();
+
+ mRootView = findViewById(R.id.media_activity_root);
mAppBarView = findViewById(R.id.app_bar);
mAppBarView.setListener(mAppBarListener);
- mAppBarView.setContentForwardEnabled(mContentForwardBrowseEnabled);
+ mediaSourceViewModel.getPrimaryMediaSource().observe(this,
+ this::onMediaSourceChanged);
+
+ MediaAppSelectorWidget appSelector = findViewById(R.id.app_switch_container);
+ appSelector.setFragmentActivity(this);
+ SearchBar searchBar = findViewById(R.id.search_bar_container);
+ searchBar.setFragmentActivity(this);
+ searchBar.setAppBarListener(mAppBarListener);
+
+ mEmptyFragment = new EmptyFragment();
+ MediaBrowserViewModel mediaBrowserViewModel = getRootBrowserViewModel();
+ mediaBrowserViewModel.getBrowseState().observe(this,
+ browseState -> {
+ mEmptyFragment.setState(browseState,
+ mediaSourceViewModel.getPrimaryMediaSource().getValue());
+ });
+ mediaBrowserViewModel.getBrowsedMediaItems().observe(this, futureData -> {
+ if (!futureData.isLoading()) {
+ if (futureData.getData() != null) {
+ mIsBrowseTreeReady = true;
+ handlePlaybackState(playbackViewModel.getPlaybackStateWrapper().getValue());
+ }
+ updateTabs(futureData.getData());
+ }
+ });
+ mediaBrowserViewModel.supportsSearch().observe(this,
+ mAppBarView::setSearchSupported);
+
mPlaybackFragment = new PlaybackFragment();
+ mPlaybackFragment.setListener(mPlaybackFragmentListener);
+ mSearchFragment = BrowseFragment.newSearchInstance();
mAppSelectionFragment = new AppSelectionFragment();
int fadeDuration = getResources().getInteger(R.integer.app_selector_fade_duration);
mAppSelectionFragment.setEnterTransition(new Fade().setDuration(fadeDuration));
mAppSelectionFragment.setExitTransition(new Fade().setDuration(fadeDuration));
- mActiveMediaSourceManager = new ActiveMediaSourceManager(this);
- mPlaybackModel = new PlaybackModel(this);
- mMediaSourcesManager = new MediaSourcesManager(this);
- mAlbumBackground = findViewById(R.id.media_background);
- mPlaybackControls = findViewById(R.id.browse_controls);
- mPlaybackControls.setModel(mPlaybackModel);
- mMetadataView = findViewById(R.id.browse_metadata);
- mMetadataView.setModel(mPlaybackModel);
- mBrowseControlsContainer = findViewById(R.id.browse_controls_container);
- mBrowseControlsContainer.setOnClickListener(view -> switchToMode(Mode.PLAYBACK));
- TypedValue outValue = new TypedValue();
- getResources().getValue(R.dimen.playback_background_blur_radius, outValue, true);
- mBackgroundBlurRadius = outValue.getFloat();
- getResources().getValue(R.dimen.playback_background_blur_scale, outValue, true);
- mBackgroundBlurScale = outValue.getFloat();
- mSharedPreferences = getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE);
- mFadeDuration = getResources().getInteger(
- R.integer.new_album_art_fade_in_duration);
- mEmptyFragment = new EmptyFragment();
+
+ MinimizedPlaybackControlBar browsePlaybackControls =
+ findViewById(R.id.minimized_playback_controls);
+ browsePlaybackControls.setModel(playbackViewModel, this);
+
+ mMiniPlaybackControls = findViewById(R.id.minimized_playback_controls);
+ mMiniPlaybackControls.setOnClickListener(view -> changeMode(Mode.PLAYBACK));
+
+ mFadeDuration = getResources().getInteger(R.integer.new_album_art_fade_in_duration);
mBrowseContainer = findViewById(R.id.fragment_container);
+ mErrorContainer = findViewById(R.id.error_container);
mPlaybackContainer = findViewById(R.id.playback_container);
+ mSearchContainer = findViewById(R.id.search_container);
getSupportFragmentManager().beginTransaction()
.replace(R.id.playback_container, mPlaybackFragment)
.commit();
- }
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.search_container, mSearchFragment)
+ .commit();
- @Override
- public void onResume() {
- super.onResume();
- mPlaybackModel.registerObserver(mPlaybackObserver);
- mActiveMediaSourceManager.registerObserver(mActiveSourceObserver);
- mMediaSourcesManager.registerObserver(mMediaSourcesManagerObserver);
- handleIntent();
- }
+ playbackViewModel.getPlaybackController().observe(this,
+ playbackController -> {
+ if (playbackController != null) playbackController.prepare();
+ mPlaybackController = playbackController;
+ });
- @Override
- public void onPause() {
- super.onPause();
- mPlaybackModel.unregisterObserver(mPlaybackObserver);
- mActiveMediaSourceManager.unregisterObserver(mActiveSourceObserver);
- mMediaSourcesManager.unregisterObserver(mMediaSourcesManagerObserver);
+ playbackViewModel.getPlaybackStateWrapper().observe(this, this::handlePlaybackState);
+
+ mCarUxRestrictionsUtil = CarUxRestrictionsUtil.getInstance(this);
+ mRestrictions = CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP;
+ mCarUxRestrictionsUtil.register(mListener);
+
+ mPlaybackContainer.setOnTouchListener(new ClosePlaybackDetector(this));
}
@Override
- public void onDestroy() {
+ protected void onDestroy() {
+ mCarUxRestrictionsUtil.unregister(mListener);
super.onDestroy();
- mDrawerController.cleanup();
- mPlaybackControls.setModel(null);
- mMetadataView.setModel(null);
}
- @Override
- protected CarDrawerAdapter getRootAdapter() {
- return mDrawerController == null ? null : mDrawerController.getRootAdapter();
+ private boolean isUxRestricted() {
+ return CarUxRestrictionsUtil.isRestricted(mRestrictions, mActiveCarUxRestrictions);
}
- @Override
- protected void onNewIntent(Intent intent) {
- super.onNewIntent(intent);
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, "onNewIntent(); intent: " + (intent == null ? "<< NULL >>" : intent));
+ private void handlePlaybackState(PlaybackViewModel.PlaybackStateWrapper state) {
+ // TODO(arnaudberry) rethink interactions between customized layouts and dynamic visibility.
+ mCanShowMiniPlaybackControls = (state != null) && state.shouldDisplay();
+
+ if (mMode != Mode.PLAYBACK) {
+ ViewUtils.setVisible(mMiniPlaybackControls, mCanShowMiniPlaybackControls);
+ getInnerViewModel().setMiniControlsVisible(mCanShowMiniPlaybackControls);
+ }
+ if (state == null) {
+ return;
+ }
+ if (mCurrentPlaybackState == null || mCurrentPlaybackState != state.getState()) {
+ mCurrentPlaybackState = state.getState();
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "handlePlaybackState(); state change: " + mCurrentPlaybackState);
+ }
}
- setIntent(intent);
- handleIntent();
- }
+ Bundle extras = state.getExtras();
+ PendingIntent intent = extras == null ? null : extras.getParcelable(
+ MediaConstants.ERROR_RESOLUTION_ACTION_INTENT);
- @Override
- public void onBackPressed() {
- mPlaybackFragment.closeOverflowMenu();
- super.onBackPressed();
- }
+ String label = extras == null ? null : extras.getString(
+ MediaConstants.ERROR_RESOLUTION_ACTION_LABEL);
- private void onBrowseConnected(boolean success) {
- if (!success) {
- updateTabs(null);
- mMediaSource.unsubscribeChildren(null, mItemsSubscription);
- mMediaSource.unsubscribe(mMediaSourceObserver);
- updateBrowseFragment(BrowseState.ERROR, null);
- return;
+ String displayedMessage = null;
+ if (!TextUtils.isEmpty(state.getErrorMessage())) {
+ displayedMessage = state.getErrorMessage().toString();
+ } else if (state.getErrorCode() != PlaybackStateCompat.ERROR_CODE_UNKNOWN_ERROR) {
+ // TODO(b/131601881): convert the error codes to prebuilt error messages
+ displayedMessage = getString(R.string.default_error_message);
+ } else if (state.getState() == PlaybackStateCompat.STATE_ERROR) {
+ displayedMessage = getString(R.string.default_error_message);
}
- mMediaSource.subscribeChildren(null, mItemsSubscription);
- if (mPlaybackModel.getMediaController() == null) {
- mPlaybackModel.setMediaController(mMediaSource.getMediaController());
+
+ if (!TextUtils.isEmpty(displayedMessage)) {
+ if (mIsBrowseTreeReady) {
+ if (intent != null && !isUxRestricted()) {
+ showDialog(intent, displayedMessage, label, getString(android.R.string.cancel));
+ } else {
+ showToast(displayedMessage);
+ }
+ } else {
+ mErrorFragment = ErrorFragment.newInstance(displayedMessage, label, intent);
+ setErrorFragment(mErrorFragment);
+ changeMode(Mode.FATAL_ERROR);
+ }
}
}
- private void handleIntent() {
- Intent intent = getIntent();
- String action = intent != null ? intent.getAction() : null;
-
- getDrawerController().closeDrawer();
-
- if (Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE.equals(action)) {
- // The user either wants to browse a particular media source or switch to the
- // playback UI.
- String packageName = intent.getStringExtra(KEY_MEDIA_PACKAGE);
- if (packageName != null) {
- // We were told to navigate to a particular package: we open browse for it.
- closeAppSelector();
- changeMediaSource(new MediaSource(this, packageName), null);
- switchToMode(Mode.BROWSING);
- return;
- }
+ private void showDialog(PendingIntent intent, String message, String positiveBtnText,
+ String negativeButtonText) {
+ AlertDialog.Builder dialog = new AlertDialog.Builder(this);
+ dialog.setMessage(message)
+ .setNegativeButton(negativeButtonText, null)
+ .setPositiveButton(positiveBtnText, (dialogInterface, i) -> {
+ try {
+ intent.send();
+ } catch (PendingIntent.CanceledException e) {
+ if (Log.isLoggable(TAG, Log.ERROR)) {
+ Log.e(TAG, "Pending intent canceled");
+ }
+ }
+ })
+ .show();
+ }
- // If we didn't receive a package name and we are playing something: show the playback
- // UI for the playing media source.
- MediaController controller = mActiveMediaSourceManager.getMediaController();
- if (controller != null) {
- closeAppSelector();
- changeMediaSource(new MediaSource(this, controller.getPackageName()), controller);
- switchToMode(Mode.PLAYBACK);
- return;
- }
- }
+ private void showToast(String message) {
+ maybeCancelToast();
+ mToast = Toast.makeText(this, message, Toast.LENGTH_LONG);
+ mToast.show();
+ }
- // In any other case, if we were already browsing something: just close drawers/overlays
- // and display what we have.
- if (mMediaSource != null) {
- closeAppSelector();
- return;
+ private void maybeCancelToast() {
+ if (mToast != null) {
+ mToast.cancel();
+ mToast = null;
}
+ }
- // If we don't have a current media source, we try with the last one we remember.
- MediaSource lastMediaSource = getLastMediaSource();
- if (lastMediaSource != null) {
- closeAppSelector();
- changeMediaSource(lastMediaSource, null);
- switchToMode(Mode.BROWSING);
+ @Override
+ public void onBackPressed() {
+ super.onBackPressed();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ MediaSource mediaSource = getMediaSourceViewModel().getPrimaryMediaSource().getValue();
+ if (mediaSource == null) {
+ mAppBarView.openAppSelector();
} else {
- // If we don't have anything from before: open the app selector.
- openAppSelector();
+ mAppBarView.closeAppSelector();
}
}
/**
* Sets the media source being browsed.
*
- * @param mediaSource the media source we are going to try to browse
- * @param controller a controller we can use to control the playback state of the given
- * source. If not provided, we will try to obtain it from the session manager.
- * Otherwise, we will obtain a controller once the media browser is connected.
+ * @param mediaSource the new media source we are going to try to browse
*/
- private void changeMediaSource(MediaSource mediaSource, MediaController controller) {
- if (Objects.equals(mediaSource, mMediaSource)) {
- // No change, nothing to do.
- return;
- }
- if (mMediaSource != null) {
- mMediaSource.unsubscribeChildren(null, mItemsSubscription);
- mMediaSource.unsubscribe(mMediaSourceObserver);
- updateTabs(new ArrayList<>());
- }
- mMediaSource = mediaSource;
- mPlaybackModel.setMediaController(controller != null ? controller
- : mActiveMediaSourceManager.getControllerForPackage(mediaSource.getPackageName()));
- setLastMediaSource(mMediaSource);
- if (mMediaSource != null) {
+ private void onMediaSourceChanged(@Nullable MediaSource mediaSource) {
+ mIsBrowseTreeReady = false;
+ maybeCancelToast();
+ if (mediaSource != null) {
if (Log.isLoggable(TAG, Log.INFO)) {
Log.i(TAG, "Browsing: " + mediaSource.getName());
}
- // Prepare the media source for playback
- mPlaybackModel.onPrepare();
- // Make the drawer display browse information of the selected source
- ComponentName component = mMediaSource.getBrowseServiceComponentName();
- MediaManager.getInstance(this).setMediaClientComponent(component);
- // If content forward browsing is disabled, then no need to subscribe to this media
- // source, we will use the drawer instead.
- if (mContentForwardBrowseEnabled) {
- Log.i(TAG, "Content forward is enabled: subscribing to " +
- mMediaSource.getPackageName());
- updateBrowseFragment(BrowseState.LOADING, null);
- mMediaSource.subscribe(mMediaSourceObserver);
- }
- mAppBarView.setAppIcon(mMediaSource.getRoundPackageIcon());
- mAppBarView.setTitle(mMediaSource.getName());
+ mAppBarView.setMediaAppTitle(mediaSource.getName());
+ mAppBarView.setTitle(null);
+ updateTabs(null);
+ mSearchFragment.resetSearchState();
+ changeMode(Mode.BROWSING);
+ String packageName = mediaSource.getPackageName();
+ updateSourcePreferences(packageName);
+
+ // Always go through the trampoline activity to keep all the dispatching logic there.
+ startActivity(new Intent(Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE));
} else {
- mAppBarView.setAppIcon(null);
+ mAppBarView.setMediaAppTitle(null);
mAppBarView.setTitle(null);
+ updateTabs(null);
+ updateSourcePreferences(null);
}
}
- private boolean isCurrentMediaSourcePlaying() {
- return mMediaSource != null
- && mActiveMediaSourceManager.isPlaying(mMediaSource.getPackageName());
+ private void updateSourcePreferences(@Nullable String packageName) {
+ mCurrentSourcePreferences = null;
+ if (packageName != null) {
+ Intent prefsIntent = new Intent(Intent.ACTION_APPLICATION_PREFERENCES);
+ prefsIntent.setPackage(packageName);
+ ResolveInfo info = getPackageManager().resolveActivity(prefsIntent, 0);
+ if (info != null) {
+ mCurrentSourcePreferences = new Intent(prefsIntent.getAction())
+ .setClassName(info.activityInfo.packageName, info.activityInfo.name);
+ }
+ }
+ mAppBarView.setHasSettings(mCurrentSourcePreferences != null);
}
/**
* Updates the tabs displayed on the app bar, based on the top level items on the browse tree.
- * If there is at least one browsable item, we show the browse content of that node.
- * If there are only playable items, then we show those items.
- * If there are not items at all, we show the empty message.
- * If we receive null, we show the error message.
+ * If there is at least one browsable item, we show the browse content of that node. If there
+ * are only playable items, then we show those items. If there are not items at all, we show the
+ * empty message. If we receive null, we show the error message.
*
* @param items top level items, or null if there was an error trying load those items.
*/
private void updateTabs(List<MediaItemMetadata> items) {
if (items == null || items.isEmpty()) {
+ mAppBarView.setActiveItem(null);
mAppBarView.setItems(null);
- updateBrowseFragment(items == null ? BrowseState.ERROR : BrowseState.EMPTY, null);
+ setCurrentFragment(mEmptyFragment);
+ mBrowseFragment = null;
+ mTopItems = items;
return;
}
- items = customizeTabs(mMediaSource, items);
+ if (Objects.equals(mTopItems, items)) {
+ // When coming back to the app, the live data sends an update even if the list hasn't
+ // changed. Updating the tabs then recreates the browse fragment, which produces jank
+ // (b/131830876), and also resets the navigation to the top of the first tab...
+ return;
+ }
+ mTopItems = items;
+
List<MediaItemMetadata> browsableTopLevel = items.stream()
- .filter(item -> item.isBrowsable())
+ .filter(MediaItemMetadata::isBrowsable)
.collect(Collectors.toList());
- if (!browsableTopLevel.isEmpty()) {
- // If we have at least a few browsable items, we show the tabs
- mAppBarView.setItems(browsableTopLevel);
- updateBrowseFragment(BrowseState.LOADED, browsableTopLevel.get(0));
- } else {
- // Otherwise, we show the top of the tree with no fabs
+ if (browsableTopLevel.size() == 1) {
+ // If there is only a single tab, use it as a header instead
+ mAppBarView.setMediaAppTitle(browsableTopLevel.get(0).getTitle());
+ mAppBarView.setTitle(null);
mAppBarView.setItems(null);
- updateBrowseFragment(BrowseState.LOADED, null);
+ } else {
+ mAppBarView.setItems(browsableTopLevel);
}
+ showTopItem(browsableTopLevel.isEmpty() ? null : browsableTopLevel.get(0));
}
- /**
- * Extension point used to customize media items displayed on the tabs.
- *
- * @param mediaSource media source these items belong to.
- * @param items items to override.
- * @return an updated list of items.
- * @deprecated This method will be removed on b/79089344
- */
- @Deprecated
- protected List<MediaItemMetadata> customizeTabs(MediaSource mediaSource,
- List<MediaItemMetadata> items) {
- return items;
+ private void showTopItem(@Nullable MediaItemMetadata topItem) {
+ mBrowseFragment = BrowseFragment.newInstance(topItem);
+ setCurrentFragment(mBrowseFragment);
+ mAppBarView.setActiveItem(topItem);
+ }
+
+ private void setErrorFragment(Fragment fragment) {
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.error_container, fragment)
+ .commitAllowingStateLoss();
+ }
+
+ private void setCurrentFragment(Fragment fragment) {
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.fragment_container, fragment)
+ .commitAllowingStateLoss();
}
- private void switchToMode(Mode mode) {
- // If content forward is not enable, then we always show the playback UI (browse will be
- // done in the drawer)
- mMode = mContentForwardBrowseEnabled ? mode : Mode.PLAYBACK;
- updateMetadata();
- switch (mMode) {
+ @Nullable
+ private BrowseFragment getCurrentBrowseFragment() {
+ return mMode == Mode.SEARCHING ? mSearchFragment : mBrowseFragment;
+ }
+
+ private void changeMode(Mode mode) {
+ if (mMode == mode) return;
+
+ if (Log.isLoggable(TAG, Log.INFO)) {
+ Log.i(TAG, "Changing mode from: " + mMode+ " to: " + mode);
+ }
+
+ Mode oldMode = mMode;
+ getInnerViewModel().saveMode(mode);
+ mMode = mode;
+
+ if (mode == Mode.FATAL_ERROR) {
+ ViewUtils.showViewAnimated(mErrorContainer, mFadeDuration);
+ ViewUtils.hideViewAnimated(mPlaybackContainer, mFadeDuration);
+ ViewUtils.hideViewAnimated(mBrowseContainer, mFadeDuration);
+ ViewUtils.hideViewAnimated(mSearchContainer, mFadeDuration);
+ mAppBarView.setState(AppBarView.State.EMPTY);
+ return;
+ }
+
+ updateMetadata(mode);
+
+ switch (mode) {
case PLAYBACK:
+ mPlaybackContainer.setY(0);
+ mPlaybackContainer.setAlpha(0f);
+ ViewUtils.hideViewAnimated(mErrorContainer, mFadeDuration);
ViewUtils.showViewAnimated(mPlaybackContainer, mFadeDuration);
ViewUtils.hideViewAnimated(mBrowseContainer, mFadeDuration);
- mAppBarView.setState(AppBarView.State.PLAYING);
+ ViewUtils.hideViewAnimated(mSearchContainer, mFadeDuration);
+ ViewUtils.hideViewAnimated(mAppBarView, mFadeDuration);
break;
case BROWSING:
- ViewUtils.hideViewAnimated(mPlaybackContainer, mFadeDuration);
- ViewUtils.showViewAnimated(mBrowseContainer, mFadeDuration);
- mAppBarView.setState(AppBarView.State.BROWSING);
- break;
- }
- }
-
- /**
- * Updates the browse area with either a loading state, the root node content, or the
- * content of a particular media item.
- *
- * @param state state in the process of loading browse information.
- * @param topItem if state == IDLE, this will contain the item to display,
- * or null to display the root node.
- */
- private void updateBrowseFragment(BrowseState state, MediaItemMetadata topItem) {
- switch(state) {
- case LOADED:
- if (topItem != null) {
- mCurrentFragment = BrowseFragment.newInstance(mMediaSource, topItem);
- mAppBarView.setActiveItem(topItem);
+ if (oldMode == Mode.PLAYBACK) {
+ ViewUtils.hideViewAnimated(mErrorContainer, 0);
+ ViewUtils.showViewAnimated(mBrowseContainer, 0);
+ ViewUtils.hideViewAnimated(mSearchContainer, 0);
+ ViewUtils.showViewAnimated(mAppBarView, 0);
+ mPlaybackContainer.animate()
+ .translationY(mRootView.getHeight())
+ .setDuration(mFadeDuration)
+ .setListener(ViewUtils.hideViewAfterAnimation(mPlaybackContainer))
+ .start();
} else {
- mCurrentFragment = BrowseFragment.newInstance(mMediaSource, null);
- mAppBarView.setActiveItem(null);
+ ViewUtils.hideViewAnimated(mErrorContainer, mFadeDuration);
+ ViewUtils.hideViewAnimated(mPlaybackContainer, mFadeDuration);
+ ViewUtils.showViewAnimated(mBrowseContainer, mFadeDuration);
+ ViewUtils.hideViewAnimated(mSearchContainer, mFadeDuration);
+ ViewUtils.showViewAnimated(mAppBarView, mFadeDuration);
}
+ updateAppBar();
break;
- case EMPTY:
- case LOADING:
- case ERROR:
- mCurrentFragment = mEmptyFragment;
- mEmptyFragment.setState(state, mMediaSource);
- mAppBarView.setActiveItem(null);
+ case SEARCHING:
+ ViewUtils.hideViewAnimated(mErrorContainer, mFadeDuration);
+ ViewUtils.hideViewAnimated(mPlaybackContainer, mFadeDuration);
+ ViewUtils.hideViewAnimated(mBrowseContainer, mFadeDuration);
+ ViewUtils.showViewAnimated(mSearchContainer, mFadeDuration);
+ ViewUtils.showViewAnimated(mAppBarView, mFadeDuration);
+ updateAppBar();
break;
}
- getSupportFragmentManager().beginTransaction()
- .replace(R.id.fragment_container, mCurrentFragment)
- .commitAllowingStateLoss();
}
- private void updateMetadata() {
- if (isCurrentMediaSourcePlaying()) {
- if (mMode == Mode.PLAYBACK) {
- ViewUtils.hideViewAnimated(mBrowseControlsContainer, mFadeDuration);
- } else {
- ViewUtils.showViewAnimated(mBrowseControlsContainer, mFadeDuration);
- }
- MediaItemMetadata metadata = mPlaybackModel.getMetadata();
- if (Objects.equals(mCurrentMetadata, metadata)) {
- return;
- }
- mCurrentMetadata = metadata;
- mUpdateAlbumArtRunnable.run();
- } else {
- mAlbumBackground.setImageBitmap(null, true);
- ViewUtils.hideViewAnimated(mBrowseControlsContainer, mFadeDuration);
- }
+ private void updateAppBar() {
+ BrowseFragment fragment = getCurrentBrowseFragment();
+ boolean isStacked = fragment != null && !fragment.isAtTopStack();
+ AppBarView.State unstackedState = mMode == Mode.SEARCHING
+ ? AppBarView.State.SEARCHING
+ : AppBarView.State.BROWSING;
+ mAppBarView.setTitle(isStacked ? fragment.getCurrentMediaItem().getTitle() : null);
+ mAppBarView.setState(isStacked ? AppBarView.State.STACKED : unstackedState);
}
- /**
- * We might receive new album art before we are ready to display it. If that situation happens
- * we will retrieve and render the album art when the views are already laid out.
- */
- private Runnable mUpdateAlbumArtRunnable = new Runnable() {
- @Override
- public void run() {
- MediaItemMetadata metadata = mPlaybackModel.getMetadata();
- if (metadata != null) {
- if (mAlbumBackground.getWidth() == 0 || mAlbumBackground.getHeight() == 0) {
- // We need to wait for the view to be measured before we can render this
- // album art.
- mAlbumBackground.setImageBitmap(null, false);
- mAlbumBackground.post(this);
- } else {
- mAlbumBackground.removeCallbacks(this);
- metadata.getAlbumArt(MediaActivity.this,
- mAlbumBackground.getWidth(),
- mAlbumBackground.getHeight(),
- false)
- .thenAccept(bitmap -> setBackgroundImage(bitmap));
- }
- } else {
- mAlbumBackground.removeCallbacks(this);
- mAlbumBackground.setImageBitmap(null, true);
+ private void updateMetadata(Mode mode) {
+ if (mode != Mode.PLAYBACK) {
+ mPlaybackFragment.closeOverflowMenu();
+ if (mCanShowMiniPlaybackControls) {
+ ViewUtils.showViewAnimated(mMiniPlaybackControls, mFadeDuration);
+ getInnerViewModel().setMiniControlsVisible(true);
}
}
- };
-
- private void setBackgroundImage(Bitmap bitmap) {
- // TODO(b/77551865): Implement image blurring once the following issue is solved:
- // b/77551557
- // bitmap = ImageUtils.blur(getContext(), bitmap, mBackgroundBlurScale,
- // mBackgroundBlurRadius);
- mAlbumBackground.setImageBitmap(bitmap, true);
- }
-
- @Override
- public MediaSource getMediaSource(String packageName) {
- if (mMediaSource != null && mMediaSource.getPackageName().equals(packageName)) {
- return mMediaSource;
- }
- return new MediaSource(this, packageName);
}
@Override
public void onBackStackChanged() {
- // TODO: Update ActionBar
+ updateAppBar();
}
@Override
- public void onPlayableItemClicked(MediaSource mediaSource, MediaItemMetadata item) {
- mPlaybackModel.onStop();
- if (!Objects.equals(mediaSource, mPlaybackModel.getMediaSource())) {
- Log.w(TAG, "Trying to play an item from a different source "
- + "(expected: " + mPlaybackModel.getMediaSource() + ", received"
- + mediaSource + ")");
- changeMediaSource(mediaSource, mediaSource.getMediaController());
- }
- mPlaybackModel.onPlayItem(item.getId());
+ public void onPlayableItemClicked(MediaItemMetadata item) {
+ mPlaybackController.stop();
+ mPlaybackController.playItem(item.getId());
+ boolean switchToPlayback = getResources().getBoolean(
+ R.bool.switch_to_playback_view_when_playable_item_is_clicked);
+ if (switchToPlayback) {
+ changeMode(Mode.PLAYBACK);
+ } else if (mMode == Mode.SEARCHING) {
+ changeMode(Mode.BROWSING);
+ }
setIntent(null);
}
- private void openAppSelector() {
- mIsAppSelectorOpen = true;
- FragmentManager manager = getSupportFragmentManager();
- mAppBarView.setState(AppBarView.State.APP_SELECTION);
- manager.beginTransaction()
- .replace(R.id.app_selection_container, mAppSelectionFragment)
- .commit();
+ public MediaSourceViewModel getMediaSourceViewModel() {
+ return MediaSourceViewModel.get(getApplication());
}
- private void closeAppSelector() {
- mIsAppSelectorOpen = false;
- FragmentManager manager = getSupportFragmentManager();
- mAppBarView.setState(mMode == Mode.PLAYBACK ? AppBarView.State.PLAYING
- : AppBarView.State.BROWSING);
- manager.beginTransaction()
- .remove(mAppSelectionFragment)
- .commit();
+ public PlaybackViewModel getPlaybackViewModel() {
+ return PlaybackViewModel.get(getApplication());
}
- @Override
- public List<MediaSource> getMediaSources() {
- return mMediaSourcesManager.getMediaSources()
- .stream()
- .filter(source -> source.getMediaBrowser() != null || source.isCustom())
- .collect(Collectors.toList());
+ private MediaBrowserViewModel getRootBrowserViewModel() {
+ return MediaBrowserViewModel.Factory.getInstanceForBrowseRoot(getMediaSourceViewModel(),
+ ViewModelProviders.of(this));
+ }
+
+ public ViewModel getInnerViewModel() {
+ return ViewModelProviders.of(this).get(ViewModel.class);
}
@Override
- public void onMediaSourceSelected(MediaSource mediaSource) {
- closeAppSelector();
- if (mediaSource.getMediaBrowser() != null && !mediaSource.isCustom()) {
- mCurrentMetadata = null;
- changeMediaSource(mediaSource, null);
- switchToMode(Mode.BROWSING);
- } else {
- String packageName = mediaSource.getPackageName();
- Intent intent = getPackageManager().getLaunchIntentForPackage(packageName);
- startActivity(intent);
- }
+ public AppBarView getAppBar() {
+ return mAppBarView;
}
- private MediaSource getLastMediaSource() {
- String packageName = mSharedPreferences.getString(LAST_MEDIA_SOURCE_SHARED_PREF_KEY, null);
- if (packageName == null) {
- return null;
+ public static class ViewModel extends AndroidViewModel {
+ private boolean mNeedsInitialization = true;
+ private PlaybackViewModel mPlaybackViewModel;
+ /** Saves the Mode across config changes. */
+ private Mode mSavedMode;
+
+ private MutableLiveData<Boolean> mIsMiniControlsVisible = new MutableLiveData<>();
+
+ public ViewModel(@NonNull Application application) {
+ super(application);
}
- // Verify that the stored package name corresponds to a currently installed media source.
- for (MediaSource mediaSource : mMediaSourcesManager.getMediaSources()) {
- if (mediaSource.getPackageName().equals(packageName)) {
- return mediaSource;
+
+ void init(@NonNull PlaybackViewModel playbackViewModel) {
+ if (mPlaybackViewModel == playbackViewModel) {
+ return;
}
+ mPlaybackViewModel = playbackViewModel;
+ mSavedMode = Mode.BROWSING;
+ mNeedsInitialization = false;
}
- return null;
- }
- private void setLastMediaSource(@NonNull MediaSource mediaSource) {
- mSharedPreferences.edit()
- .putString(LAST_MEDIA_SOURCE_SHARED_PREF_KEY, mediaSource.getPackageName())
- .apply();
- }
+ boolean needsInitialization() {
+ return mNeedsInitialization;
+ }
+ void setMiniControlsVisible(boolean visible) {
+ mIsMiniControlsVisible.setValue(visible);
+ }
- @Override
- public PlaybackModel getPlaybackModel() {
- return mPlaybackModel;
+ LiveData<Boolean> getMiniControlsVisible() {
+ return mIsMiniControlsVisible;
+ }
+
+ void saveMode(Mode mode) {
+ mSavedMode = mode;
+ }
+
+ Mode getSavedMode() {
+ return mSavedMode;
+ }
}
- @Override
- public void onQueueButtonClicked() {
- if (mContentForwardBrowseEnabled) {
- mPlaybackFragment.toggleQueueVisibility();
- } else {
- mDrawerController.showPlayQueue();
+
+ private class ClosePlaybackDetector extends GestureDetector.SimpleOnGestureListener
+ implements View.OnTouchListener {
+
+ private final ViewConfiguration mViewConfig;
+ private final GestureDetectorCompat mDetector;
+
+
+ ClosePlaybackDetector(Context context) {
+ mViewConfig = ViewConfiguration.get(context);
+ mDetector = new GestureDetectorCompat(context, this);
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ return mDetector.onTouchEvent(event);
+ }
+
+ @Override
+ public boolean onDown(MotionEvent event) {
+ return (mMode == Mode.PLAYBACK);
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float vX, float vY) {
+ float dY = e2.getY() - e1.getY();
+ if (dY > mViewConfig.getScaledTouchSlop() &&
+ Math.abs(vY) > mViewConfig.getScaledMinimumFlingVelocity()) {
+ float dX = e2.getX() - e1.getX();
+ float tan = Math.abs(dX) / dY;
+ if (tan <= 0.58) { // Accept 30 degrees on each side of the down vector.
+ changeMode(Mode.BROWSING);
+ }
+ }
+ return true;
}
}
}
diff --git a/src/com/android/car/media/MediaConstants.java b/src/com/android/car/media/MediaConstants.java
deleted file mode 100644
index a0e190a..0000000
--- a/src/com/android/car/media/MediaConstants.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.media;
-
-import android.annotation.StringDef;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * Constants shared by SDK and 3rd party media apps to extend the media APIs in order to accommodate
- * car-specific requirements.
- * These constants are also shared with Android Auto Projected. They will be moved to the Car
- * Support Library on b/76108793.
- * Please refer to <a href="https://developer.android.com/training/auto/audio/index.html">Providing
- * Audio Playback for Auto</a> for a detailed explanation.
- */
-public class MediaConstants {
- /**
- * Action along with the media connection broadcast, which contains the current media
- * connection status.
- */
- public static final String ACTION_MEDIA_STATUS = "com.google.android.gms.car.media.STATUS";
-
- /**
- * Key for media connection status in extra.
- */
- public static final String MEDIA_CONNECTION_STATUS = "media_connection_status";
-
- @Retention(RetentionPolicy.SOURCE)
- @StringDef({MEDIA_CONNECTED, MEDIA_DISCONNECTED})
- public @interface ConnectionType {}
-
- /**
- * Type of connection status: current media is connected.
- */
- public static final String MEDIA_CONNECTED = "media_connected";
-
- /**
- * Type of connection status: current media is disconnected.
- */
- public static final String MEDIA_DISCONNECTED = "media_disconnected";
-
- /**
- * Key for extra feedback message in extra.
- */
- public static final String EXTRA_CUSTOM_ACTION_STATUS = "media_custom_action_status";
-
- /**
- * Extra along with the Media Session, which contains if the slot of the action should be
- * always reserved for the queue action.
- */
- public static final String EXTRA_RESERVED_SLOT_QUEUE =
- "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE";
-
- /**
- * Extra along with the Media Session, which contains if the slot of the action should be
- * always reserved for the skip to previous action.
- */
- public static final String EXTRA_RESERVED_SLOT_SKIP_TO_PREVIOUS =
- "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS";
-
- /**
- * Extra along with the Media Session, which contains if the slot of the action should be
- * always reserved for the skip to next action.
- */
- public static final String EXTRA_RESERVED_SLOT_SKIP_TO_NEXT =
- "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT";
-
- /**
- * Extra along with custom action playback state to indicate a repeated action.
- */
- public static final String EXTRA_REPEATED_CUSTOM_ACTION_BUTTON =
- "com.google.android.gms.car.media.CUSTOM_ACTION.REPEATED_ACTIONS";
-
- /**
- * Extra along with custom action playback state to indicate a repeated custom action button
- * state.
- */
- public static final String EXTRA_REPEATED_CUSTOM_ACTION_BUTTON_ON_DOWN =
- "com.google.android.gms.car.media.CUSTOM_ACTION.ON_DOWN_EVENT";
-
- /**
- * Extra along with metadata to indicate whether this audio is advertisement.
- */
- public static final String EXTRA_METADATA_ADVERTISEMENT =
- "android.media.metadata.ADVERTISEMENT";
-}
diff --git a/src/com/android/car/media/MediaDispatcherActivity.java b/src/com/android/car/media/MediaDispatcherActivity.java
new file mode 100644
index 0000000..f0cff92
--- /dev/null
+++ b/src/com/android/car/media/MediaDispatcherActivity.java
@@ -0,0 +1,62 @@
+package com.android.car.media;
+
+import android.car.Car;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.car.media.common.source.MediaSource;
+import com.android.car.media.common.source.MediaSourceViewModel;
+
+
+/**
+ * A trampoline activity that handles the {@link Car#CAR_INTENT_ACTION_MEDIA_TEMPLATE} implicit
+ * intent, and fires up either the Media Center's {@link MediaActivity}, or the specialized
+ * application if the selected media source is custom (e.g. the Radio app).
+ */
+public class MediaDispatcherActivity extends FragmentActivity {
+
+ private static final String TAG = "MediaDispatcherActivity";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Intent intent = getIntent();
+ String action = intent != null ? intent.getAction() : null;
+
+ MediaSourceViewModel mediaSrcVM = MediaSourceViewModel.get(getApplication());
+ MediaSource mediaSrc = null;
+
+ if (Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE.equals(action)) {
+ String packageName = intent.getStringExtra(Car.CAR_EXTRA_MEDIA_PACKAGE);
+ if (packageName != null) {
+ mediaSrc = new MediaSource(this, packageName);
+ mediaSrcVM.setPrimaryMediaSource(mediaSrc);
+ }
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onCreate packageName : " + packageName);
+ }
+ }
+
+ if (mediaSrc == null) {
+ mediaSrc = mediaSrcVM.getPrimaryMediaSource().getValue();
+ }
+
+ Intent newIntent;
+ if ((mediaSrc != null) && (!mediaSrc.isBrowsable() || mediaSrc.isCustom())) {
+ // Launch custom app (e.g. Radio)
+ String srcPackage = mediaSrc.getPackageName();
+ newIntent = getPackageManager().getLaunchIntentForPackage(srcPackage);
+ } else {
+ // Launch media center
+ newIntent = new Intent(this, MediaActivity.class);
+ }
+
+ newIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(newIntent);
+ finish();
+ }
+}
diff --git a/src/com/android/car/media/MediaManager.java b/src/com/android/car/media/MediaManager.java
deleted file mode 100644
index 4da5187..0000000
--- a/src/com/android/car/media/MediaManager.java
+++ /dev/null
@@ -1,597 +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.media;
-
-import android.app.SearchManager;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.pm.ServiceInfo;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.media.browse.MediaBrowser;
-import android.media.session.MediaController;
-import android.media.session.MediaSession;
-import android.media.session.PlaybackState;
-import android.os.Bundle;
-import android.service.media.MediaBrowserService;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Manages which media app we should connect to. The manager also retrieves various attributes
- * from the media app and share among different components in GearHead media app.
- *
- * @deprecated This manager is being replaced by {@link com.android.car.media.common.PlaybackModel}.
- */
-@Deprecated
-public class MediaManager {
- private static final String TAG = "GH.MediaManager";
- private static final String PREFS_FILE_NAME = "MediaClientManager.Preferences";
- /** The package of the most recently used media component **/
- private static final String PREFS_KEY_PACKAGE = "media_package";
- /** The class of the most recently used media class **/
- private static final String PREFS_KEY_CLASS = "media_class";
- /** Third-party defined application theme to use **/
- private static final String THEME_META_DATA_NAME = "com.google.android.gms.car.application.theme";
-
- public static final String KEY_MEDIA_COMPONENT = "media_component";
- /** Intent extra specifying the package with the MediaBrowser **/
- public static final String KEY_MEDIA_PACKAGE = "media_package";
- /** Intent extra specifying the MediaBrowserService **/
- public static final String KEY_MEDIA_CLASS = "media_class";
-
- /**
- * Flag for when GSA is not 100% confident on the query and therefore, the result in the
- * {@link #KEY_MEDIA_PACKAGE_FROM_GSA} should be ignored.
- */
- private static final String KEY_IGNORE_ORIGINAL_PKG =
- "com.google.android.projection.gearhead.ignore_original_pkg";
-
- /**
- * Intent extra specifying the package name of the media app that should handle
- * {@link android.provider.MediaStore#INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH}. This must match
- * KEY_PACKAGE defined in ProjectionIntentStarter in GSA.
- */
- public static final String KEY_MEDIA_PACKAGE_FROM_GSA =
- "android.car.intent.extra.MEDIA_PACKAGE";
-
- private static final String GOOGLE_PLAY_MUSIC_PACKAGE = "com.google.android.music";
- // Extras along with the Knowledge Graph that are not meant to be seen by external apps.
- private static final String[] INTERNAL_EXTRAS = {"KEY_LAUNCH_HANDOVER_UNDERNEATH",
- "com.google.android.projection.gearhead.ignore_original_pkg"};
-
- private static final Intent MEDIA_BROWSER_INTENT =
- new Intent(MediaBrowserService.SERVICE_INTERFACE);
- private static MediaManager sInstance;
-
- private final MediaController.Callback mMediaControllerCallback =
- new MediaManagerCallback(this);
- private final MediaBrowser.ConnectionCallback mMediaBrowserConnectionCallback =
- new MediaManagerConnectionCallback(this);
-
- public interface Listener {
- void onMediaAppChanged(ComponentName componentName);
-
- /**
- * Called when we want to show a message on playback screen.
- * @param msg if null, dismiss any previous message and
- * restore the track title and subtitle.
- */
- void onStatusMessageChanged(String msg);
- }
-
- /**
- * An adapter interface to abstract the specifics of how media services are queried. This allows
- * for Vanagon to query for allowed media services without the need to connect to carClientApi.
- */
- public interface ServiceAdapter {
- List<ResolveInfo> queryAllowedServices(Intent providerIntent);
- }
-
- private int mPrimaryColor;
- private int mPrimaryColorDark;
- private int mAccentColor;
- private CharSequence mName;
-
- private final Context mContext;
- private final List<Listener> mListeners = new ArrayList<>();
-
- private ServiceAdapter mServiceAdapter;
- private Intent mPendingSearchIntent;
-
- private MediaController mController;
- private MediaBrowser mBrowser;
- private ComponentName mCurrentComponent;
- private PendingMsg mPendingMsg;
-
- public synchronized static MediaManager getInstance(Context context) {
- if (sInstance == null) {
- sInstance = new MediaManager(context.getApplicationContext());
- }
- return sInstance;
- }
-
- private MediaManager(Context context) {
- mContext = context;
-
- // Set some sane default values for the attributes
- mName = "";
- int color = context.getResources().getColor(android.R.color.background_dark);
- mPrimaryColor = color;
- mAccentColor = color;
- mPrimaryColorDark = color;
- }
-
- /**
- * Returns the default component used to load media.
- */
- public ComponentName getDefaultComponent(ServiceAdapter serviceAdapter) {
- SharedPreferences prefs = mContext
- .getSharedPreferences(PREFS_FILE_NAME, Context.MODE_PRIVATE);
- String packageName = prefs.getString(PREFS_KEY_PACKAGE, null);
- String className = prefs.getString(PREFS_KEY_CLASS, null);
- final Intent providerIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
- List<ResolveInfo> mediaApps = serviceAdapter.queryAllowedServices(providerIntent);
-
- // check if the previous component we connected to is still valid.
- if (packageName != null && className != null) {
- boolean componentValid = false;
- for (ResolveInfo info : mediaApps) {
- if (info.serviceInfo.packageName.equals(packageName)
- && info.serviceInfo.name.equals(className)) {
- componentValid = true;
- }
- }
- // if not valid, null it and we will bring up the lens switcher or connect to another
- // app (this may happen when the app has been uninstalled)
- if (!componentValid) {
- packageName = null;
- className = null;
- }
- }
-
- // If there are no apps used before or previous app is not valid,
- // try to connect to a supported media app.
- if (packageName == null || className == null) {
- // Only one app installed, connect to it.
- if (mediaApps.size() == 1) {
- ResolveInfo info = mediaApps.get(0);
- packageName = info.serviceInfo.packageName;
- className = info.serviceInfo.name;
- } else {
- // there are '0' or >1 media apps installed; don't know what to run
- return null;
- }
- }
- return new ComponentName(packageName, className);
- }
-
- /**
- * Connects to the most recently used media app if it exists and return true.
- * Otherwise check the number of supported media apps installed,
- * if only one installed, connect to it return true. Otherwise return false.
- */
- public boolean connectToMostRecentMediaComponent(ServiceAdapter serviceAdapter) {
- ComponentName component = getDefaultComponent(serviceAdapter);
- if (component != null) {
- setMediaClientComponent(serviceAdapter, component);
- return true;
- }
- return false;
- }
-
- public ComponentName getCurrentComponent() {
- return mCurrentComponent;
- }
-
- public void setMediaClientComponent(ComponentName component) {
- setMediaClientComponent(null, component);
- }
-
- /**
- * Change the media component. This will connect to a {@link android.media.browse.MediaBrowser} if necessary.
- * All registered listener will be updated with the new component.
- */
- public void setMediaClientComponent(ServiceAdapter serviceAdapter, ComponentName component) {
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, "setMediaClientComponent(), "
- + "component: " + (component == null ? "<< NULL >>" : component.toString()));
- }
-
- if (component == null) {
- return;
- }
-
- // mController will be set to null if previously connected media session has crashed.
- if (mCurrentComponent != null && mCurrentComponent.equals(component)
- && mController != null) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Already connected to " + component.toString());
- }
- return;
- }
-
- mCurrentComponent = component;
- mServiceAdapter = serviceAdapter;
- disconnectCurrentBrowser();
- updateClientPackageAttributes(mCurrentComponent);
-
- if (mController != null) {
- mController.unregisterCallback(mMediaControllerCallback);
- mController = null;
- }
- mBrowser = new MediaBrowser(mContext, component, mMediaBrowserConnectionCallback, null);
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Connecting to " + component.toString());
- }
- mBrowser.connect();
-
- writeComponentToPrefs(component);
-
- ArrayList<Listener> temp = new ArrayList<Listener>(mListeners);
- for (Listener listener : temp) {
- listener.onMediaAppChanged(mCurrentComponent);
- }
- }
-
- /**
- * Processes the search intent using the current media app. If it's not connected yet, store it
- * in the {@code mPendingSearchIntent} and process it when the app is connected.
- *
- * @param intent The intent containing the query and
- * MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH action
- */
- public void processSearchIntent(Intent intent) {
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, "processSearchIntent(), query: "
- + (intent == null ? "<< NULL >>" : intent.getStringExtra(SearchManager.QUERY)));
- }
- if (intent == null) {
- return;
- }
- mPendingSearchIntent = intent;
-
- String mediaPackageName;
- if (intent.getBooleanExtra(KEY_IGNORE_ORIGINAL_PKG, false)) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Ignoring package from gsa and falling back to default media app");
- }
- mediaPackageName = null;
- } else if (intent.hasExtra(KEY_MEDIA_PACKAGE_FROM_GSA)) {
- // Legacy way of piping through the media app package.
- mediaPackageName = intent.getStringExtra(KEY_MEDIA_PACKAGE_FROM_GSA);
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Package from extras: " + mediaPackageName);
- }
- } else {
- mediaPackageName = intent.getPackage();
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Package from getPackage(): " + mediaPackageName);
- }
- }
-
- if (mediaPackageName != null && mCurrentComponent != null
- && !mediaPackageName.equals(mCurrentComponent.getPackageName())) {
- final ComponentName componentName =
- getMediaBrowserComponent(mServiceAdapter, mediaPackageName);
- if (componentName == null) {
- Log.w(TAG, "There are no matching media app to handle intent: " + intent);
- return;
- }
- setMediaClientComponent(mServiceAdapter, componentName);
- // It's safe to return here as pending search intent will be processed
- // when newly created media controller for the new media component is connected.
- return;
- }
-
- String query = mPendingSearchIntent.getStringExtra(SearchManager.QUERY);
- if (mController != null) {
- mController.getTransportControls().pause();
- mPendingMsg = new PendingMsg(PendingMsg.STATUS_UPDATE,
- mContext.getResources().getString(R.string.loading));
- notifyStatusMessage(mPendingMsg.mMsg);
- Bundle extras = mPendingSearchIntent.getExtras();
- // Remove two extras that are not meant to be seen by external apps.
- if (!GOOGLE_PLAY_MUSIC_PACKAGE.equals(mediaPackageName)) {
- for (String key : INTERNAL_EXTRAS) {
- extras.remove(key);
- }
- }
- mController.getTransportControls().playFromSearch(query, extras);
- mPendingSearchIntent = null;
- } else {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "No controller for search intent; save it for later");
- }
- }
- }
-
-
- private ComponentName getMediaBrowserComponent(ServiceAdapter serviceAdapter,
- final String packageName) {
- List<ResolveInfo> queryResults = serviceAdapter.queryAllowedServices(MEDIA_BROWSER_INTENT);
- if (queryResults != null) {
- for (int i = 0, N = queryResults.size(); i < N; ++i) {
- final ResolveInfo ri = queryResults.get(i);
- if (ri != null && ri.serviceInfo != null
- && ri.serviceInfo.packageName.equals(packageName)) {
- return new ComponentName(ri.serviceInfo.packageName, ri.serviceInfo.name);
- }
- }
- }
- return null;
- }
-
- /**
- * Add a listener to get media app changes.
- * Your listener will be called with the initial values when the listener is added.
- */
- public void addListener(Listener listener) {
- mListeners.add(listener);
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, "addListener(); count: " + mListeners.size());
- }
-
- if (mCurrentComponent != null) {
- listener.onMediaAppChanged(mCurrentComponent);
- }
-
- if (mPendingMsg != null) {
- listener.onStatusMessageChanged(mPendingMsg.mMsg);
- }
- }
-
- public void removeListener(Listener listener) {
- mListeners.remove(listener);
-
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, "removeListener(); count: " + mListeners.size());
- }
-
- if (mListeners.size() == 0) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "no manager listeners; destroy manager instance");
- }
-
- synchronized (MediaManager.class) {
- sInstance = null;
- }
-
- if (mBrowser != null) {
- mBrowser.disconnect();
- }
- }
- }
-
- public CharSequence getMediaClientName() {
- return mName;
- }
-
- public int getMediaClientPrimaryColor() {
- return mPrimaryColor;
- }
-
- public int getMediaClientPrimaryColorDark() {
- return mPrimaryColorDark;
- }
-
- public int getMediaClientAccentColor() {
- return mAccentColor;
- }
-
- private void writeComponentToPrefs(ComponentName componentName) {
- // Store selected media service to shared preference.
- SharedPreferences prefs = mContext
- .getSharedPreferences(PREFS_FILE_NAME, Context.MODE_PRIVATE);
- SharedPreferences.Editor editor = prefs.edit();
- editor.putString(PREFS_KEY_PACKAGE, componentName.getPackageName());
- editor.putString(PREFS_KEY_CLASS, componentName.getClassName());
- editor.apply();
- }
-
- /**
- * Disconnect from the current media browser service if any, and notify the listeners.
- */
- private void disconnectCurrentBrowser() {
- if (mBrowser != null) {
- mBrowser.disconnect();
- mBrowser = null;
- }
- }
-
- private void updateClientPackageAttributes(ComponentName componentName) {
- TypedArray ta = null;
- try {
- String packageName = componentName.getPackageName();
- ApplicationInfo applicationInfo =
- mContext.getPackageManager().getApplicationInfo(packageName,
- PackageManager.GET_META_DATA);
- ServiceInfo serviceInfo = mContext.getPackageManager().getServiceInfo(
- componentName, PackageManager.GET_META_DATA);
-
- // Get the proper app name, check service label, then application label.
- CharSequence name = "";
- if (serviceInfo.labelRes != 0) {
- name = serviceInfo.loadLabel(mContext.getPackageManager());
- } else if (applicationInfo.labelRes != 0) {
- name = applicationInfo.loadLabel(mContext.getPackageManager());
- }
- if (TextUtils.isEmpty(name)) {
- name = mContext.getResources().getString(R.string.unknown_media_provider_name);
- }
- mName = name;
-
- // Get the proper theme, check theme for service, then application.
- int appTheme = 0;
- if (serviceInfo.metaData != null) {
- appTheme = serviceInfo.metaData.getInt(THEME_META_DATA_NAME);
- }
- if (appTheme == 0 && applicationInfo.metaData != null) {
- appTheme = applicationInfo.metaData.getInt(THEME_META_DATA_NAME);
- }
- if (appTheme == 0) {
- appTheme = applicationInfo.theme;
- }
-
- Context packageContext = mContext.createPackageContext(packageName, 0);
- packageContext.setTheme(appTheme);
- Resources.Theme theme = packageContext.getTheme();
- ta = theme.obtainStyledAttributes(new int[] {
- android.R.attr.colorPrimary,
- android.R.attr.colorAccent,
- android.R.attr.colorPrimaryDark
- });
- int defaultColor =
- mContext.getResources().getColor(android.R.color.background_dark);
- mPrimaryColor = ta.getColor(0, defaultColor);
- mAccentColor = ta.getColor(1, defaultColor);
- mPrimaryColorDark = ta.getColor(2, defaultColor);
- } catch (PackageManager.NameNotFoundException e) {
- Log.e(TAG, "Unable to update media client package attributes.", e);
- } finally {
- if (ta != null) {
- ta.recycle();
- }
- }
- }
-
- private void notifyStatusMessage(String str) {
- for (Listener l : mListeners) {
- l.onStatusMessageChanged(str);
- }
- }
-
- private void doPlaybackStateChanged(PlaybackState playbackState) {
- // Display error message in MediaPlaybackFragment.
- if (mPendingMsg == null) {
- return;
- }
- // Dismiss the error msg if any,
- // and dismiss status update msg if the state is now playing
- if ((mPendingMsg.mType == PendingMsg.ERROR) ||
- (playbackState.getState() == PlaybackState.STATE_PLAYING
- && mPendingMsg.mType == PendingMsg.STATUS_UPDATE)) {
- mPendingMsg = null;
- notifyStatusMessage(null);
- }
- }
-
- private void doOnSessionDestroyed() {
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, "Media session destroyed");
- }
- if (mController != null) {
- mController.unregisterCallback(mMediaControllerCallback);
- }
- mController = null;
- mServiceAdapter = null;
- }
-
- private void doOnConnected() {
- // existing mController has been disconnected before we call MediaBrowser.connect()
- MediaSession.Token token = mBrowser.getSessionToken();
- if (token == null) {
- Log.e(TAG, "Media session token is null");
- return;
- }
- mController = new MediaController(mContext, token);
- mController.registerCallback(mMediaControllerCallback);
- processSearchIntent(mPendingSearchIntent);
- }
-
- private void doOnConnectionFailed() {
- Log.w(TAG, "Media browser connection FAILED!");
- // disconnect anyway to make sure we get into a sanity state
- mBrowser.disconnect();
- mBrowser = null;
- }
-
- private static class PendingMsg {
- public static final int ERROR = 0;
- public static final int STATUS_UPDATE = 1;
-
- public int mType;
- public String mMsg;
- public PendingMsg(int type, String msg) {
- mType = type;
- mMsg = msg;
- }
- }
-
- private static class MediaManagerCallback extends MediaController.Callback {
- private final WeakReference<MediaManager> mWeakCallback;
-
- MediaManagerCallback(MediaManager callback) {
- mWeakCallback = new WeakReference<>(callback);
- }
-
- @Override
- public void onPlaybackStateChanged(PlaybackState playbackState) {
- MediaManager callback = mWeakCallback.get();
- if (callback == null) {
- return;
- }
- callback.doPlaybackStateChanged(playbackState);
- }
-
- @Override
- public void onSessionDestroyed() {
- MediaManager callback = mWeakCallback.get();
- if (callback == null) {
- return;
- }
- callback.doOnSessionDestroyed();
- }
- }
-
- private static class MediaManagerConnectionCallback extends MediaBrowser.ConnectionCallback {
- private final WeakReference<MediaManager> mWeakCallback;
-
- private MediaManagerConnectionCallback(MediaManager callback) {
- mWeakCallback = new WeakReference<>(callback);
- }
-
- @Override
- public void onConnected() {
- MediaManager callback = mWeakCallback.get();
- if (callback == null) {
- return;
- }
- callback.doOnConnected();
- }
-
- @Override
- public void onConnectionSuspended() {}
-
- @Override
- public void onConnectionFailed() {
- MediaManager callback = mWeakCallback.get();
- if (callback == null) {
- return;
- }
- callback.doOnConnectionFailed();
- }
- }
-}
diff --git a/src/com/android/car/media/MediaPlaybackModel.java b/src/com/android/car/media/MediaPlaybackModel.java
deleted file mode 100644
index aef706a..0000000
--- a/src/com/android/car/media/MediaPlaybackModel.java
+++ /dev/null
@@ -1,409 +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.media;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.media.MediaMetadata;
-import android.media.browse.MediaBrowser;
-import android.media.session.MediaController;
-import android.media.session.MediaSession;
-import android.media.session.PlaybackState;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.Log;
-
-import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.car.apps.common.util.Assert;
-
-import java.util.ArrayList;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.function.Consumer;
-
-/**
- * A model for controlling media playback. This model will take care of all Media Manager, Browser,
- * and controller connection and callbacks. On each stage of the connection, error, or disconnect
- * this model will call back to the presenter. All call backs to the presenter will be done on the
- * main thread. Intended to provide a much more usable model interface to UI code.
- *
- * @deprecated This model is being replaced by {@link com.android.car.media.common.PlaybackModel}.
- */
-@Deprecated
-public class MediaPlaybackModel {
- private static final String TAG = "MediaPlaybackModel";
-
- private final Context mContext;
- private final Bundle mBrowserExtras;
- private final List<MediaPlaybackModel.Listener> mListeners = new LinkedList<>();
-
- private Handler mHandler;
- private MediaController mController;
- private MediaBrowser mBrowser;
- private int mPrimaryColor;
- private int mPrimaryColorDark;
- private int mAccentColor;
- private ComponentName mCurrentComponentName;
- private Resources mPackageResources;
-
- /**
- * This is the interface to listen to {@link MediaPlaybackModel} callbacks. All callbacks are
- * done in the main thread.
- */
- public interface Listener {
- /** Indicates active media app has changed. A new mediaBrowser is now connecting to the new
- * app and mediaController has been released, pending connection to new service.
- */
- void onMediaAppChanged(@Nullable ComponentName currentName,
- @Nullable ComponentName newName);
- void onMediaAppStatusMessageChanged(@Nullable String message);
-
- /**
- * Indicates the mediaBrowser is not connected and mediaController is available.
- */
- void onMediaConnected();
- /**
- * Indicates mediaBrowser connection is temporarily suspended.
- * */
- void onMediaConnectionSuspended();
- /**
- * Indicates that the MediaBrowser connected failed. The mediaBrowser and controller have
- * now been released.
- */
- void onMediaConnectionFailed(CharSequence failedMediaClientName);
- void onPlaybackStateChanged(@Nullable PlaybackState state);
- void onMetadataChanged(@Nullable MediaMetadata metadata);
- void onQueueChanged(List<MediaSession.QueueItem> queue);
- /**
- * Indicates that the MediaSession was destroyed. The mediaController has been released.
- */
- void onSessionDestroyed(CharSequence destroyedMediaClientName);
- }
-
- /** Convenient Listener base class for extension */
- public static abstract class AbstractListener implements Listener {
- @Override
- public void onMediaAppChanged(@Nullable ComponentName currentName,
- @Nullable ComponentName newName) {}
- @Override
- public void onMediaAppStatusMessageChanged(@Nullable String message) {}
- @Override
- public void onMediaConnected() {}
- @Override
- public void onMediaConnectionSuspended() {}
- @Override
- public void onMediaConnectionFailed(CharSequence failedMediaClientName) {}
- @Override
- public void onPlaybackStateChanged(@Nullable PlaybackState state) {}
- @Override
- public void onMetadataChanged(@Nullable MediaMetadata metadata) {}
- @Override
- public void onQueueChanged(List<MediaSession.QueueItem> queue) {}
- @Override
- public void onSessionDestroyed(CharSequence destroyedMediaClientName) {}
- }
-
- public MediaPlaybackModel(Context context, Bundle browserExtras) {
- mContext = context;
- mBrowserExtras = browserExtras;
- mHandler = new Handler(Looper.getMainLooper());
- }
-
- @MainThread
- public void start() {
- Assert.isMainThread();
- MediaManager.getInstance(mContext).addListener(mMediaManagerListener);
- }
-
- @MainThread
- public void stop() {
- Assert.isMainThread();
- MediaManager.getInstance(mContext).removeListener(mMediaManagerListener);
- if (mBrowser != null) {
- mBrowser.disconnect();
- mBrowser = null;
- }
- if (mController != null) {
- mController.unregisterCallback(mMediaControllerCallback);
- mController = null;
- }
- // Calling this with null will clear queue of callbacks and message. This needs to be done
- // here because prior to the above lines to disconnect and unregister the browser and
- // controller a posted runnable to do work maybe have happened and thus we need to clear it
- // out to prevent race conditions.
- mHandler.removeCallbacksAndMessages(null);
- }
-
- @MainThread
- public void addListener(MediaPlaybackModel.Listener listener) {
- Assert.isMainThread();
- mListeners.add(listener);
- }
-
- @MainThread
- public void removeListener(MediaPlaybackModel.Listener listener) {
- Assert.isMainThread();
- mListeners.remove(listener);
- }
-
- @MainThread
- private void notifyListeners(Consumer<Listener> callback) {
- Assert.isMainThread();
- // Clone mListeners in case any of the callbacks made triggers a listener to be added or
- // removed to/from mListeners.
- List<Listener> listenersCopy = new LinkedList<>(mListeners);
- // Invokes callback.accept(listener) for each listener.
- listenersCopy.forEach(callback);
- }
-
- @MainThread
- public Resources getPackageResources() {
- Assert.isMainThread();
- return mPackageResources;
- }
-
- @MainThread
- public int getPrimaryColor() {
- Assert.isMainThread();
- return mPrimaryColor;
- }
-
- @MainThread
- public int getAccentColor() {
- Assert.isMainThread();
- return mAccentColor;
- }
-
- @MainThread
- public int getPrimaryColorDark() {
- Assert.isMainThread();
- return mPrimaryColorDark;
- }
-
- @MainThread
- public MediaMetadata getMetadata() {
- Assert.isMainThread();
- if (mController == null) {
- return null;
- }
- return mController.getMetadata();
- }
-
- @MainThread
- public @NonNull List<MediaSession.QueueItem> getQueue() {
- Assert.isMainThread();
- if (mController == null) {
- return new ArrayList<>();
- }
- List<MediaSession.QueueItem> currentQueue = mController.getQueue();
- if (currentQueue == null) {
- currentQueue = new ArrayList<>();
- }
- return currentQueue;
- }
-
- @MainThread
- public PlaybackState getPlaybackState() {
- Assert.isMainThread();
- if (mController == null) {
- return null;
- }
- return mController.getPlaybackState();
- }
-
- /**
- * Return true if the slot of the action should be always reserved for it,
- * even when the corresponding playbackstate action is disabled. This avoids
- * an undesired reflow on the playback drawer when a temporary state
- * disables some action. This information can be set on the MediaSession
- * extras as a boolean for each default action that needs its slot
- * reserved. Currently supported actions are ACTION_SKIP_TO_PREVIOUS,
- * ACTION_SKIP_TO_NEXT and ACTION_SHOW_QUEUE.
- */
- @MainThread
- public boolean isSlotForActionReserved(String actionExtraKey) {
- Assert.isMainThread();
- if (mController != null) {
- Bundle extras = mController.getExtras();
- if (extras != null) {
- return extras.getBoolean(actionExtraKey, false);
- }
- }
- return false;
- }
-
- @MainThread
- public boolean isConnected() {
- Assert.isMainThread();
- return mController != null;
- }
-
- @MainThread
- public MediaBrowser getMediaBrowser() {
- Assert.isMainThread();
- return mBrowser;
- }
-
- @MainThread
- public MediaController.TransportControls getTransportControls() {
- Assert.isMainThread();
- if (mController == null) {
- return null;
- }
- return mController.getTransportControls();
- }
-
- @MainThread
- public @NonNull CharSequence getQueueTitle() {
- Assert.isMainThread();
- if (mController == null) {
- return "";
- }
- return mController.getQueueTitle();
- }
-
- private final MediaManager.Listener mMediaManagerListener = new MediaManager.Listener() {
- @Override
- public void onMediaAppChanged(final ComponentName name) {
- mHandler.post(() -> {
- if (mBrowser != null) {
- mBrowser.disconnect();
- }
- mBrowser = new MediaBrowser(mContext, name, mConnectionCallback, mBrowserExtras);
- try {
- mPackageResources = mContext.getPackageManager().getResourcesForApplication(
- name.getPackageName());
- } catch (PackageManager.NameNotFoundException e) {
- Log.e(TAG, "Unable to get resources for " + name.getPackageName());
- }
-
- if (mController != null) {
- mController.unregisterCallback(mMediaControllerCallback);
- mController = null;
- }
-
- final ComponentName currentName = mCurrentComponentName;
- notifyListeners((listener) -> listener.onMediaAppChanged(currentName, name));
- mCurrentComponentName = name;
-
- mBrowser.connect();
-
- // reset the colors and views if we switch to another app.
- MediaManager manager = MediaManager.getInstance(mContext);
- mPrimaryColor = manager.getMediaClientPrimaryColor();
- mAccentColor = manager.getMediaClientAccentColor();
- mPrimaryColorDark = manager.getMediaClientPrimaryColorDark();
- });
- }
-
- @Override
- public void onStatusMessageChanged(final String message) {
- mHandler.post(() -> {
- notifyListeners((listener) -> listener.onMediaAppStatusMessageChanged(message));
- });
- }
- };
-
- private final MediaBrowser.ConnectionCallback mConnectionCallback =
- new MediaBrowser.ConnectionCallback() {
- @Override
- public void onConnected() {
- // Existing mController has already been disconnected before we call
- // MediaBrowser.connect()
- // getSessionToken returns a non null token
- MediaSession.Token token = mBrowser.getSessionToken();
- if (mController != null) {
- mController.unregisterCallback(mMediaControllerCallback);
- }
- mController = new MediaController(mContext, token);
- mController.registerCallback(mMediaControllerCallback);
- notifyListeners(Listener::onMediaConnected);
- }
-
- @Override
- public void onConnectionSuspended() {
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, "Media browser service connection suspended."
- + " Waiting to be reconnected....");
- }
- notifyListeners(Listener::onMediaConnectionSuspended);
- }
-
- @Override
- public void onConnectionFailed() {
- // disconnect anyway to make sure we get into a sanity state
- mBrowser.disconnect();
- mBrowser = null;
- mCurrentComponentName = null;
-
- CharSequence failedClientName = MediaManager.getInstance(mContext)
- .getMediaClientName();
- notifyListeners(
- (listener) -> listener.onMediaConnectionFailed(failedClientName));
- }
- };
-
- private final MediaController.Callback mMediaControllerCallback =
- new MediaController.Callback() {
- @Override
- public void onPlaybackStateChanged(final PlaybackState state) {
- mHandler.post(() -> {
- notifyListeners((listener) -> listener.onPlaybackStateChanged(state));
- });
- }
-
- @Override
- public void onMetadataChanged(final MediaMetadata metadata) {
- mHandler.post(() -> {
- notifyListeners((listener) -> listener.onMetadataChanged(metadata));
- });
- }
-
- @Override
- public void onQueueChanged(final List<MediaSession.QueueItem> queue) {
- mHandler.post(() -> {
- final List<MediaSession.QueueItem> currentQueue =
- queue != null ? queue : new ArrayList<>();
- notifyListeners((listener) -> listener.onQueueChanged(currentQueue));
- });
- }
-
- @Override
- public void onSessionDestroyed() {
- mHandler.post(() -> {
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, "onSessionDestroyed()");
- }
- mCurrentComponentName = null;
- if (mController != null) {
- mController.unregisterCallback(mMediaControllerCallback);
- mController = null;
- }
-
- CharSequence destroyedClientName = MediaManager.getInstance(
- mContext).getMediaClientName();
- notifyListeners(
- (listener) -> listener.onSessionDestroyed(destroyedClientName));
- });
- }
- };
-}
diff --git a/src/com/android/car/media/MetadataController.java b/src/com/android/car/media/MetadataController.java
deleted file mode 100644
index 59e4d9d..0000000
--- a/src/com/android/car/media/MetadataController.java
+++ /dev/null
@@ -1,177 +0,0 @@
-package com.android.car.media;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.media.session.PlaybackState;
-import android.support.v4.media.session.PlaybackStateCompat;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.SeekBar;
-import android.widget.TextView;
-
-import com.android.car.media.common.MediaItemMetadata;
-import com.android.car.media.common.PlaybackModel;
-
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Locale;
-
-/**
- * Common controller for displaying current track's metadata.
- */
-public class MetadataController {
-
- private static final DateFormat TIME_FORMAT = new SimpleDateFormat("m:ss", Locale.US);
-
- @NonNull
- private final TextView mTitle;
- @NonNull
- private final TextView mSubtitle;
- @Nullable
- private final TextView mTime;
- @NonNull
- private final SeekBar mSeekBar;
- @Nullable
- private final ImageView mAlbumArt;
-
- @Nullable
- private PlaybackModel mModel;
-
- private boolean mUpdatesPaused;
- private boolean mNeedsMetadataUpdate;
- private int mAlbumArtSize;
-
- private final PlaybackModel.PlaybackObserver mPlaybackObserver =
- new PlaybackModel.PlaybackObserver() {
- @Override
- public void onPlaybackStateChanged() {
- updateState();
- }
-
- @Override
- public void onSourceChanged() {
- updateState();
- updateMetadata();
- }
-
- @Override
- public void onMetadataChanged() {
- updateMetadata();
- }
- };
-
- /**
- * Create a new MetadataController that operates on the provided Views
- * @param title Displays the track's title. Must not be {@code null}.
- * @param subtitle Displays the track's artist. Must not be {@code null}.
- * @param time Displays the track's progress as text. May be {@code null}.
- * @param seekBar Displays the track's progress visually. Must not be {@code null}.
- * @param albumArt Displays the track's album art. May be {@code null}.
- */
- public MetadataController(@NonNull TextView title, @NonNull TextView subtitle,
- @Nullable TextView time, @NonNull SeekBar seekBar, @Nullable ImageView albumArt) {
- mTitle = title;
- mSubtitle = subtitle;
- mTime = time;
- mSeekBar = seekBar;
- mSeekBar.setOnTouchListener((view, event) -> true);
- mAlbumArt = albumArt;
- mAlbumArtSize = title.getContext().getResources()
- .getDimensionPixelSize(R.dimen.playback_album_art_size_large);
- }
-
- /**
- * Registers the {@link PlaybackModel} this widget will use to follow playback state.
- * Consumers of this class must unregister the {@link PlaybackModel} by calling this method with
- * null.
- *
- * @param model {@link PlaybackModel} to subscribe, or null to unsubscribe.
- */
- public void setModel(@Nullable PlaybackModel model) {
- if (mModel != null) {
- mModel.unregisterObserver(mPlaybackObserver);
- }
- mModel = model;
- if (mModel != null) {
- mModel.registerObserver(mPlaybackObserver);
- }
- }
-
- private void updateState() {
- updateProgress();
-
- mSeekBar.removeCallbacks(mSeekBarRunnable);
- if (mModel != null && mModel.isPlaying()) {
- mSeekBar.post(mSeekBarRunnable);
- }
- }
-
- private void updateMetadata() {
- if(mUpdatesPaused) {
- mNeedsMetadataUpdate = true;
- return;
- }
-
- mNeedsMetadataUpdate = false;
- MediaItemMetadata metadata = mModel != null ? mModel.getMetadata() : null;
- mTitle.setText(metadata != null ? metadata.getTitle() : null);
- mSubtitle.setText(metadata != null ? metadata.getSubtitle() : null);
- if (mAlbumArt != null && metadata != null && (metadata.getAlbumArtUri() != null
- || metadata.getAlbumArtBitmap() != null)) {
- mAlbumArt.setVisibility(View.VISIBLE);
- metadata.getAlbumArt(mAlbumArt.getContext(), mAlbumArtSize, mAlbumArtSize, true)
- .thenAccept(mAlbumArt::setImageBitmap);
- } else if (mAlbumArt != null) {
- mAlbumArt.setVisibility(View.GONE);
- }
- }
-
- private static final long SEEK_BAR_UPDATE_TIME_INTERVAL_MS = 1000;
-
- private final Runnable mSeekBarRunnable = new Runnable() {
- @Override
- public void run() {
- if (mModel == null || !mModel.isPlaying()) {
- return;
- }
- updateProgress();
- mSeekBar.postDelayed(this, SEEK_BAR_UPDATE_TIME_INTERVAL_MS);
-
- }
- };
-
- private void updateProgress() {
- if (mModel == null) {
- mTime.setVisibility(View.INVISIBLE);
- mSeekBar.setVisibility(View.INVISIBLE);
- return;
- }
- long maxProgress = mModel.getMaxProgress();
- long progress = mModel.getProgress();
- int visibility = maxProgress > 0 && progress != PlaybackState.PLAYBACK_POSITION_UNKNOWN
- ? View.VISIBLE : View.INVISIBLE;
- if (mTime != null) {
- String time = String.format("%s / %s",
- TIME_FORMAT.format(new Date(progress)),
- TIME_FORMAT.format(new Date(maxProgress)));
- mTime.setVisibility(visibility);
- mTime.setText(time);
- }
- mSeekBar.setVisibility(visibility);
- mSeekBar.setMax((int) maxProgress);
- mSeekBar.setProgress((int) progress);
- }
-
-
- public void pauseUpdates() {
- mUpdatesPaused = true;
- }
-
- public void resumeUpdates() {
- mUpdatesPaused = false;
- if (mNeedsMetadataUpdate) {
- updateMetadata();
- }
- }
-}
diff --git a/src/com/android/car/media/PlaybackFragment.java b/src/com/android/car/media/PlaybackFragment.java
index 376c304..7ecb661 100644
--- a/src/com/android/car/media/PlaybackFragment.java
+++ b/src/com/android/car/media/PlaybackFragment.java
@@ -16,14 +16,15 @@
package com.android.car.media;
-import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.Context;
-import android.media.session.MediaController;
+import android.content.res.ColorStateList;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
import android.os.Bundle;
-import android.transition.Transition;
-import android.transition.TransitionInflater;
-import android.transition.TransitionListenerAdapter;
-import android.transition.TransitionManager;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -31,86 +32,202 @@ import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;
-import androidx.car.widget.ListItem;
-import androidx.car.widget.ListItemAdapter;
-import androidx.car.widget.ListItemProvider;
-import androidx.car.widget.PagedListView;
-import androidx.car.widget.TextListItem;
+import androidx.annotation.NonNull;
import androidx.constraintlayout.widget.ConstraintLayout;
-import androidx.constraintlayout.widget.ConstraintSet;
import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.DefaultItemAnimator;
+import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
+import com.android.car.apps.common.BackgroundImageView;
+import com.android.car.apps.common.util.ViewUtils;
+import com.android.car.media.common.MediaAppSelectorWidget;
import com.android.car.media.common.MediaItemMetadata;
-import com.android.car.media.common.MediaSource;
-import com.android.car.media.common.PlaybackControls;
-import com.android.car.media.common.PlaybackModel;
+import com.android.car.media.common.MetadataController;
+import com.android.car.media.common.PlaybackControlsActionBar;
+import com.android.car.media.common.playback.PlaybackViewModel;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
-import java.util.stream.Collectors;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
/**
* A {@link Fragment} that implements both the playback and the content forward browsing experience.
- * It observes a {@link PlaybackModel} and updates its information depending on the currently
+ * It observes a {@link PlaybackViewModel} and updates its information depending on the currently
* playing media source through the {@link android.media.session.MediaSession} API.
*/
public class PlaybackFragment extends Fragment {
private static final String TAG = "PlaybackFragment";
- private PlaybackModel mModel;
- private PlaybackControls mPlaybackControls;
+ private CompletableFuture<Bitmap> mFutureAlbumBackground;
+ private BackgroundImageView mAlbumBackground;
+ private View mBackgroundScrim;
+ private View mControlBarScrim;
+ private PlaybackControlsActionBar mPlaybackControls;
private QueueItemsAdapter mQueueAdapter;
- private PagedListView mQueue;
- private Callbacks mCallbacks;
+ private RecyclerView mQueue;
+ private ConstraintLayout mMetadataContainer;
+ private SeekBar mSeekBar;
+ private View mQueueButton;
+ private ViewGroup mNavIconContainer;
+ private List<View> mViewsToHideForCustomActions;
+
+ private DefaultItemAnimator mItemAnimator;
private MetadataController mMetadataController;
- private ConstraintLayout mRootView;
- private boolean mNeedsStateUpdate;
- private boolean mUpdatesPaused;
+ private PlaybackFragmentListener mListener;
+
+ private PlaybackViewModel.PlaybackController mController;
+ private Long mActiveQueueItemId;
+
+ private boolean mHasQueue;
private boolean mQueueIsVisible;
- private List<MediaItemMetadata> mQueueItems = new ArrayList<>();
- private PlaybackModel.PlaybackObserver mPlaybackObserver =
- new PlaybackModel.PlaybackObserver() {
- @Override
- public void onPlaybackStateChanged() {
- updateState();
- }
+ private boolean mShowTimeForActiveQueueItem;
+ private boolean mShowIconForActiveQueueItem;
+ private boolean mShowThumbnailForQueueItem;
- @Override
- public void onSourceChanged() {
- updateAccentColor();
- updateState();
- }
+ private int mFadeDuration;
+ private float mPlaybackQueueBackgroundAlpha;
- @Override
- public void onMetadataChanged() {
- }
- };
- private ListItemProvider mQueueItemsProvider = new ListItemProvider() {
- @Override
- public ListItem get(int position) {
- if (position < 0 || position >= mQueueItems.size()) {
- return null;
+ /**
+ * PlaybackFragment listener
+ */
+ public interface PlaybackFragmentListener {
+ /**
+ * Invoked when the user clicks on the collapse button
+ */
+ void onCollapse();
+ }
+
+ public class QueueViewHolder extends RecyclerView.ViewHolder {
+
+ private final View mView;
+ private final ViewGroup mThumbnailContainer;
+ private final ImageView mThumbnail;
+ private final View mSpacer;
+ private final TextView mTitle;
+ private final TextView mCurrentTime;
+ private final TextView mMaxTime;
+ private final TextView mTimeSeparator;
+ private final ImageView mActiveIcon;
+
+ QueueViewHolder(View itemView) {
+ super(itemView);
+ mView = itemView;
+ mThumbnailContainer = itemView.findViewById(R.id.thumbnail_container);
+ mThumbnail = itemView.findViewById(R.id.thumbnail);
+ mSpacer = itemView.findViewById(R.id.spacer);
+ mTitle = itemView.findViewById(R.id.title);
+ mCurrentTime = itemView.findViewById(R.id.current_time);
+ mMaxTime = itemView.findViewById(R.id.max_time);
+ mTimeSeparator = itemView.findViewById(R.id.separator);
+ mActiveIcon = itemView.findViewById(R.id.now_playing_icon);
+ }
+
+ boolean bind(MediaItemMetadata item) {
+ mView.setOnClickListener(v -> onQueueItemClicked(item));
+
+ ViewUtils.setVisible(mThumbnailContainer, mShowThumbnailForQueueItem);
+ if (mShowThumbnailForQueueItem) {
+ MediaItemMetadata.updateImageView(mThumbnail.getContext(), item, mThumbnail, 0,
+ true);
}
- MediaItemMetadata item = mQueueItems.get(position);
- TextListItem textListItem = new TextListItem(getContext());
- textListItem.setTitle(item.getTitle() != null ? item.getTitle().toString() : null);
- textListItem.setBody(item.getSubtitle() != null ? item.getSubtitle().toString() : null);
- textListItem.setOnClickListener(v -> onQueueItemClicked(item));
- return textListItem;
+ ViewUtils.setVisible(mSpacer, !mShowThumbnailForQueueItem);
+
+ mTitle.setText(item.getTitle());
+
+ boolean active = mActiveQueueItemId != null && Objects.equals(mActiveQueueItemId,
+ item.getQueueId());
+ if (active) {
+ mCurrentTime.setText(mQueueAdapter.getCurrentTime());
+ mMaxTime.setText(mQueueAdapter.getMaxTime());
+ }
+ boolean shouldShowTime =
+ mShowTimeForActiveQueueItem && active && mQueueAdapter.getTimeVisible();
+ ViewUtils.setVisible(mCurrentTime, shouldShowTime);
+ ViewUtils.setVisible(mMaxTime, shouldShowTime);
+ ViewUtils.setVisible(mTimeSeparator, shouldShowTime);
+
+ boolean shouldShowIcon = mShowIconForActiveQueueItem && active;
+ ViewUtils.setVisible(mActiveIcon, shouldShowIcon);
+
+ return active;
+ }
+ }
+
+
+ private class QueueItemsAdapter extends RecyclerView.Adapter<QueueViewHolder> {
+
+ private List<MediaItemMetadata> mQueueItems;
+ private String mCurrentTimeText;
+ private String mMaxTimeText;
+ private Integer mActiveItemPos;
+ private boolean mTimeVisible;
+
+ void setItems(@Nullable List<MediaItemMetadata> items) {
+ mQueueItems = new ArrayList<>(items != null ? items : Collections.emptyList());
+ notifyDataSetChanged();
}
+
+ void setCurrentTime(String currentTime) {
+ mCurrentTimeText = currentTime;
+ if (mActiveItemPos != null) {
+ notifyItemChanged(mActiveItemPos.intValue());
+ }
+ }
+
+ void setMaxTime(String maxTime) {
+ mMaxTimeText = maxTime;
+ if (mActiveItemPos != null) {
+ notifyItemChanged(mActiveItemPos.intValue());
+ }
+ }
+
+ void setTimeVisible(boolean visible) {
+ mTimeVisible = visible;
+ if (mActiveItemPos != null) {
+ notifyItemChanged(mActiveItemPos.intValue());
+ }
+ }
+
+ String getCurrentTime() {
+ return mCurrentTimeText;
+ }
+
+ String getMaxTime() {
+ return mMaxTimeText;
+ }
+
+ boolean getTimeVisible() {
+ return mTimeVisible;
+ }
+
@Override
- public int size() {
- return mQueueItems.size();
+ public QueueViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+ return new QueueViewHolder(inflater.inflate(R.layout.queue_list_item, parent, false));
}
- };
- private class QueueItemsAdapter extends ListItemAdapter {
- QueueItemsAdapter(Context context, ListItemProvider itemProvider) {
- super(context, itemProvider, BackgroundStyle.SOLID);
- setHasStableIds(true);
+
+ @Override
+ public void onBindViewHolder(QueueViewHolder holder, int position) {
+ int size = mQueueItems.size();
+ if (0 <= position && position < size) {
+ boolean active = holder.bind(mQueueItems.get(position));
+ if (active) {
+ mActiveItemPos = position;
+ }
+ } else {
+ Log.e(TAG, "onBindViewHolder invalid position " + position + " of " + size);
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return mQueueItems.size();
}
void refresh() {
@@ -125,153 +242,271 @@ public class PlaybackFragment extends Fragment {
}
}
- /**
- * Callbacks this fragment can trigger
- */
- public interface Callbacks {
- /**
- * Returns the playback model to use.
- */
- PlaybackModel getPlaybackModel();
+ private class QueueTopItemDecoration extends RecyclerView.ItemDecoration {
+ int mHeight;
+ int mDecorationPosition;
- /**
- * Indicates that the "show queue" button has been clicked
- */
- void onQueueButtonClicked();
- }
+ QueueTopItemDecoration(int height, int decorationPosition) {
+ mHeight = height;
+ mDecorationPosition = decorationPosition;
+ }
- private PlaybackControls.Listener mPlaybackControlsListener = new PlaybackControls.Listener() {
@Override
- public void onToggleQueue() {
- if (mCallbacks != null) {
- mCallbacks.onQueueButtonClicked();
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
+ RecyclerView.State state) {
+ super.getItemOffsets(outRect, view, parent, state);
+ if (parent.getChildAdapterPosition(view) == mDecorationPosition) {
+ outRect.top = mHeight;
}
}
- };
+ }
@Override
public View onCreateView(@NonNull LayoutInflater inflater, final ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_playback, container, false);
- mRootView = view.findViewById(R.id.playback_container);
+ mAlbumBackground = view.findViewById(R.id.playback_background);
mQueue = view.findViewById(R.id.queue_list);
+ mMetadataContainer = view.findViewById(R.id.metadata_container);
+ mSeekBar = view.findViewById(R.id.seek_bar);
+ mQueueButton = view.findViewById(R.id.queue_button);
+ mQueueButton.setOnClickListener(button -> toggleQueueVisibility());
+ mNavIconContainer = view.findViewById(R.id.nav_icon_container);
+ mNavIconContainer.setOnClickListener(nav -> onCollapse());
+ mBackgroundScrim = view.findViewById(R.id.background_scrim);
+ ViewUtils.setVisible(mBackgroundScrim, false);
+ mControlBarScrim = view.findViewById(R.id.control_bar_scrim);
+ ViewUtils.setVisible(mControlBarScrim, false);
+ mControlBarScrim.setOnClickListener(scrim -> mPlaybackControls.close());
+ mControlBarScrim.setClickable(false);
+
+ Resources res = getResources();
+ mShowTimeForActiveQueueItem = res.getBoolean(
+ R.bool.show_time_for_now_playing_queue_list_item);
+ mShowIconForActiveQueueItem = res.getBoolean(
+ R.bool.show_icon_for_now_playing_queue_list_item);
+ mShowThumbnailForQueueItem = getContext().getResources().getBoolean(
+ R.bool.show_thumbnail_for_queue_list_item);
+
+ boolean useMediaSourceColor = res.getBoolean(
+ R.bool.use_media_source_color_for_progress_bar);
+ int defaultColor = res.getColor(R.color.progress_bar_highlight, null);
+ if (useMediaSourceColor) {
+ getPlaybackViewModel().getMediaSourceColors().observe(getViewLifecycleOwner(),
+ sourceColors -> {
+ int color = sourceColors != null ? sourceColors.getAccentColor(defaultColor)
+ : defaultColor;
+ mSeekBar.setThumbTintList(ColorStateList.valueOf(color));
+ mSeekBar.setProgressTintList(ColorStateList.valueOf(color));
+ });
+ } else {
+ mSeekBar.setThumbTintList(ColorStateList.valueOf(defaultColor));
+ mSeekBar.setProgressTintList(ColorStateList.valueOf(defaultColor));
+ }
+
+ MediaAppSelectorWidget appIcon = view.findViewById(R.id.app_icon_container);
+ appIcon.setFragmentActivity(getActivity());
+ getPlaybackViewModel().getPlaybackController().observe(getViewLifecycleOwner(),
+ controller -> mController = controller);
initPlaybackControls(view.findViewById(R.id.playback_controls));
- initQueue(mQueue);
initMetadataController(view);
+ initQueue();
+
+ TypedArray hideViewIds =
+ res.obtainTypedArray(R.array.playback_views_to_hide_when_showing_custom_actions);
+ mViewsToHideForCustomActions = new ArrayList<>(hideViewIds.length());
+ for (int i = 0; i < hideViewIds.length(); i++) {
+ int viewId = hideViewIds.getResourceId(i, 0);
+ if (viewId != 0) {
+ View viewToHide = view.findViewById(viewId);
+ if (viewToHide != null) {
+ mViewsToHideForCustomActions.add(viewToHide);
+ }
+ }
+ }
+ hideViewIds.recycle();
+
+ int albumBgSizePx = getResources().getInteger(
+ com.android.car.apps.common.R.integer.background_bitmap_target_size_px);
+
+ getPlaybackViewModel().getMetadata().observe(getViewLifecycleOwner(),
+ metadata -> {
+ if (mFutureAlbumBackground != null && !mFutureAlbumBackground.isDone()) {
+ mFutureAlbumBackground.cancel(true);
+ }
+ if (metadata == null) {
+ setBackgroundImage(null);
+ mFutureAlbumBackground = null;
+ } else {
+ mFutureAlbumBackground = metadata.getAlbumArt(
+ getContext(), albumBgSizePx, albumBgSizePx, false);
+ mFutureAlbumBackground.whenComplete((result, throwable) -> {
+ if (throwable != null) {
+ setBackgroundImage(null);
+ } else {
+ setBackgroundImage(result);
+ }
+ });
+ }
+ });
+
return view;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
- mCallbacks = (Callbacks) context;
}
@Override
public void onDetach() {
super.onDetach();
- mCallbacks = null;
}
- private void initPlaybackControls(PlaybackControls playbackControls) {
+ private void setBackgroundImage(Bitmap bitmap) {
+ mAlbumBackground.setBackgroundImage(bitmap, bitmap != null);
+ }
+
+ private void initPlaybackControls(PlaybackControlsActionBar playbackControls) {
mPlaybackControls = playbackControls;
- mPlaybackControls.setListener(mPlaybackControlsListener);
- mPlaybackControls.setAnimationViewGroup(mRootView);
+ mPlaybackControls.setModel(getPlaybackViewModel(), getViewLifecycleOwner());
+ mPlaybackControls.registerExpandCollapseCallback((expanding) -> {
+ mControlBarScrim.setClickable(expanding);
+
+ Resources res = getContext().getResources();
+ int millis = expanding ? res.getInteger(R.integer.control_bar_expand_anim_duration) :
+ res.getInteger(R.integer.control_bar_collapse_anim_duration);
+
+ if (expanding) {
+ ViewUtils.showViewAnimated(mControlBarScrim, millis);
+ } else {
+ ViewUtils.hideViewAnimated(mControlBarScrim, millis);
+ }
+
+ if (!mQueueIsVisible) {
+ for (View view : mViewsToHideForCustomActions) {
+ if (expanding) {
+ ViewUtils.hideViewAnimated(view, millis);
+ } else {
+ ViewUtils.showViewAnimated(view, millis);
+ }
+ }
+ }
+ });
+ }
+
+ private void initQueue() {
+ mFadeDuration = getResources().getInteger(
+ R.integer.fragment_playback_queue_fade_duration_ms);
+ mPlaybackQueueBackgroundAlpha = getResources().getFloat(
+ R.dimen.playback_queue_background_alpha);
+
+ int decorationHeight = getResources().getDimensionPixelSize(
+ R.dimen.playback_queue_list_padding_top);
+ // Put the decoration above the first item.
+ int decorationPosition = 0;
+ mQueue.addItemDecoration(new QueueTopItemDecoration(decorationHeight, decorationPosition));
+
+ mQueue.setVerticalFadingEdgeEnabled(
+ getResources().getBoolean(R.bool.queue_fading_edge_length_enabled));
+ mQueueAdapter = new QueueItemsAdapter();
+
+ getPlaybackViewModel().getPlaybackStateWrapper().observe(getViewLifecycleOwner(),
+ state -> {
+ Long itemId = (state != null) ? state.getActiveQueueItemId() : null;
+ if (!Objects.equals(mActiveQueueItemId, itemId)) {
+ mActiveQueueItemId = itemId;
+ mQueueAdapter.refresh();
+ }
+ });
+ mQueue.setAdapter(mQueueAdapter);
+ mQueue.setLayoutManager(new LinearLayoutManager(getContext()));
+
+ // Disable item changed animation.
+ mItemAnimator = new DefaultItemAnimator();
+ mItemAnimator.setSupportsChangeAnimations(false);
+ mQueue.setItemAnimator(mItemAnimator);
+
+ getPlaybackViewModel().getQueue().observe(this, this::setQueue);
+
+ getPlaybackViewModel().hasQueue().observe(getViewLifecycleOwner(), hasQueue -> {
+ boolean enableQueue = (hasQueue != null) && hasQueue;
+ setHasQueue(enableQueue);
+ if (mQueueIsVisible && !enableQueue) {
+ toggleQueueVisibility();
+ }
+ });
+ getPlaybackViewModel().getProgress().observe(getViewLifecycleOwner(),
+ playbackProgress ->
+ {
+ mQueueAdapter.setCurrentTime(playbackProgress.getCurrentTimeText().toString());
+ mQueueAdapter.setMaxTime(playbackProgress.getMaxTimeText().toString());
+ mQueueAdapter.setTimeVisible(playbackProgress.hasTime());
+ });
}
- private void initQueue(PagedListView queueList) {
- RecyclerView recyclerView = queueList.getRecyclerView();
- recyclerView.setVerticalFadingEdgeEnabled(true);
- recyclerView.setFadingEdgeLength(getResources()
- .getDimensionPixelSize(R.dimen.car_padding_4));
- mQueueAdapter = new QueueItemsAdapter(getContext(), mQueueItemsProvider);
- queueList.setAdapter(mQueueAdapter);
+ private void setQueue(List<MediaItemMetadata> queueItems) {
+ mQueueAdapter.setItems(queueItems);
+ mQueueAdapter.refresh();
}
private void initMetadataController(View view) {
ImageView albumArt = view.findViewById(R.id.album_art);
TextView title = view.findViewById(R.id.title);
- TextView subtitle = view.findViewById(R.id.subtitle);
+ TextView artist = view.findViewById(R.id.artist);
+ TextView albumTitle = view.findViewById(R.id.album_title);
+ TextView outerSeparator = view.findViewById(R.id.outer_separator);
+ TextView curTime = view.findViewById(R.id.current_time);
+ TextView innerSeparator = view.findViewById(R.id.inner_separator);
+ TextView maxTime = view.findViewById(R.id.max_time);
SeekBar seekbar = view.findViewById(R.id.seek_bar);
- TextView time = view.findViewById(R.id.time);
- mMetadataController = new MetadataController(title, subtitle, time, seekbar, albumArt);
- }
- @Override
- public void onStart() {
- super.onStart();
- mModel = mCallbacks.getPlaybackModel();
- mMetadataController.setModel(mModel);
- mPlaybackControls.setModel(mModel);
- mModel.registerObserver(mPlaybackObserver);
- }
-
- @Override
- public void onStop() {
- super.onStop();
- mModel.unregisterObserver(mPlaybackObserver);
- mMetadataController.setModel(null);
- mPlaybackControls.setModel(null);
- mModel = null;
+ mMetadataController = new MetadataController(getViewLifecycleOwner(),
+ getPlaybackViewModel(), title, artist, albumTitle, outerSeparator,
+ curTime, innerSeparator, maxTime, seekbar, albumArt,
+ getResources().getDimensionPixelSize(R.dimen.playback_album_art_size));
}
/**
- * Hides or shows the playback queue
+ * Hides or shows the playback queue.
*/
- public void toggleQueueVisibility() {
+ private void toggleQueueVisibility() {
mQueueIsVisible = !mQueueIsVisible;
- mPlaybackControls.setQueueVisible(mQueueIsVisible);
-
- Transition transition = TransitionInflater.from(getContext()).inflateTransition(
- mQueueIsVisible ? R.transition.queue_in : R.transition.queue_out);
- transition.addListener(new TransitionListenerAdapter() {
-
- @Override
- public void onTransitionStart(Transition transition) {
- super.onTransitionStart(transition);
- mUpdatesPaused = true;
- mMetadataController.pauseUpdates();
- }
-
- @Override
- public void onTransitionEnd(Transition transition) {
- mUpdatesPaused = false;
- if (mNeedsStateUpdate) {
- updateState();
- }
- mMetadataController.resumeUpdates();
- mQueue.getRecyclerView().scrollToPosition(0);
- }
- });
- TransitionManager.beginDelayedTransition(mRootView, transition);
- ConstraintSet constraintSet = new ConstraintSet();
- constraintSet.clone(mRootView.getContext(),
- mQueueIsVisible ? R.layout.fragment_playback_with_queue : R.layout.fragment_playback);
- constraintSet.applyTo(mRootView);
+ mQueueButton.setActivated(mQueueIsVisible);
+ mQueueButton.setSelected(mQueueIsVisible);
+ if (mQueueIsVisible) {
+ ViewUtils.hideViewAnimated(mMetadataContainer, mFadeDuration);
+ ViewUtils.hideViewAnimated(mSeekBar, mFadeDuration);
+ ViewUtils.showViewAnimated(mQueue, mFadeDuration);
+ ViewUtils.showViewAnimated(mBackgroundScrim, mFadeDuration);
+ } else {
+ ViewUtils.hideViewAnimated(mQueue, mFadeDuration);
+ ViewUtils.showViewAnimated(mMetadataContainer, mFadeDuration);
+ ViewUtils.showViewAnimated(mSeekBar, mFadeDuration);
+ ViewUtils.hideViewAnimated(mBackgroundScrim, mFadeDuration);
+ }
}
- private void updateState() {
- if (mUpdatesPaused) {
- mNeedsStateUpdate = true;
- return;
- }
- mNeedsStateUpdate = false;
- mQueueItems = mModel.getQueue().stream()
- .filter(item -> item.getTitle() != null)
- .collect(Collectors.toList());
- mQueueAdapter.refresh();
+ /** Sets whether the source has a queue. */
+ private void setHasQueue(boolean hasQueue) {
+ mHasQueue = hasQueue;
+ updateQueueVisibility();
}
- private void updateAccentColor() {
- int defaultColor = getResources().getColor(android.R.color.background_dark, null);
- MediaSource mediaSource = mModel.getMediaSource();
- int color = mediaSource == null ? defaultColor : mediaSource.getAccentColor(defaultColor);
- // TODO: Update queue color
+ private void updateQueueVisibility() {
+ mQueueButton.setVisibility(mHasQueue ? View.VISIBLE : View.GONE);
}
private void onQueueItemClicked(MediaItemMetadata item) {
- mModel.onSkipToQueueItem(item.getQueueId());
+ if (mController != null) {
+ mController.skipToQueueItem(item.getQueueId());
+ }
+ boolean switchToPlayback = getResources().getBoolean(
+ R.bool.switch_to_playback_view_when_playable_item_is_clicked);
+ if (switchToPlayback) {
+ toggleQueueVisibility();
+ }
}
/**
@@ -280,4 +515,22 @@ public class PlaybackFragment extends Fragment {
public void closeOverflowMenu() {
mPlaybackControls.close();
}
+
+ private PlaybackViewModel getPlaybackViewModel() {
+ return PlaybackViewModel.get(getActivity().getApplication());
+ }
+
+ /**
+ * Sets a listener of this PlaybackFragment events. In order to avoid memory leaks, consumers
+ * must reset this reference by setting the listener to null.
+ */
+ public void setListener(PlaybackFragmentListener listener) {
+ mListener = listener;
+ }
+
+ private void onCollapse() {
+ if (mListener != null) {
+ mListener.onCollapse();
+ }
+ }
}
diff --git a/src/com/android/car/media/browse/BrowseAdapter.java b/src/com/android/car/media/browse/BrowseAdapter.java
index 631c20b..f947096 100644
--- a/src/com/android/car/media/browse/BrowseAdapter.java
+++ b/src/com/android/car/media/browse/BrowseAdapter.java
@@ -17,8 +17,6 @@
package com.android.car.media.browse;
import android.content.Context;
-import android.media.browse.MediaBrowser;
-import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -28,226 +26,97 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
+import com.android.car.media.common.MediaConstants;
import com.android.car.media.common.MediaItemMetadata;
-import com.android.car.media.common.MediaSource;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.LinkedHashMap;
+import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
-import java.util.stream.Collectors;
-
-import androidx.car.widget.PagedListView;
/**
- * A {@link RecyclerView.Adapter} that can be used to display a single level of a
- * {@link android.service.media.MediaBrowserService} media tree into a
- * {@link androidx.car.widget.PagedListView} or any other {@link RecyclerView}.
+ * A {@link RecyclerView.Adapter} that can be used to display a single level of a {@link
+ * android.service.media.MediaBrowserService} media tree into a {@link
+ * androidx.car.widget.PagedListView} or any other {@link RecyclerView}.
*
* <p>This adapter assumes that the attached {@link RecyclerView} uses a {@link GridLayoutManager},
* as it can use both grid and list elements to produce the desired representation.
*
- * <p> The actual strategy to group and expand media items has to be supplied by providing an
- * instance of {@link ContentForwardStrategy}.
- *
- * <p> The adapter will only start updating once {@link #start()} is invoked. At this point, the
- * provided {@link MediaBrowser} must be already in connected state.
- *
- * <p>Resources and asynchronous data loading must be released by callign {@link #stop()}.
- *
- * <p>No views will be actually updated until {@link #update()} is invoked (normally as a result of
- * the {@link Observer#onDirty()} event. This way, the consumer of this adapter has the opportunity
- * to decide whether updates should be displayd immediately, or if they should be delayed to
- * prevent flickering.
- *
* <p>Consumers of this adapter should use {@link #registerObserver(Observer)} to receive updates.
*/
-public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> implements
- PagedListView.DividerVisibilityManager {
+public class BrowseAdapter extends ListAdapter<BrowseViewData, BrowseViewHolder> {
private static final String TAG = "BrowseAdapter";
@NonNull
private final Context mContext;
- private final MediaSource mMediaSource;
- private final ContentForwardStrategy mCFBStrategy;
- private MediaItemMetadata mParentMediaItem;
- private LinkedHashMap<String, MediaItemState> mItemStates = new LinkedHashMap<>();
- private List<BrowseViewData> mViewData = new ArrayList<>();
- private String mParentMediaItemId;
+ @NonNull
private List<Observer> mObservers = new ArrayList<>();
- private List<MediaItemMetadata> mQueue;
- private CharSequence mQueueTitle;
+ @Nullable
+ private CharSequence mTitle;
+ @Nullable
+ private MediaItemMetadata mParentMediaItem;
private int mMaxSpanSize = 1;
- private State mState = State.IDLE;
- /**
- * Possible states of the adapter
- */
- public enum State {
- /** Loading of this item hasn't started yet */
- IDLE,
- /** There is pending information before this item can be displayed */
- LOADING,
- /** It was not possible to load metadata for this item */
- ERROR,
- /** Metadata for this items has been correctly loaded */
- LOADED
- }
+ private BrowseItemViewType mRootBrowsableViewType = BrowseItemViewType.LIST_ITEM;
+ private BrowseItemViewType mRootPlayableViewType = BrowseItemViewType.LIST_ITEM;
+
+ private static final DiffUtil.ItemCallback<BrowseViewData> DIFF_CALLBACK =
+ new DiffUtil.ItemCallback<BrowseViewData>() {
+ @Override
+ public boolean areItemsTheSame(@NonNull BrowseViewData oldItem,
+ @NonNull BrowseViewData newItem) {
+ return Objects.equals(oldItem.mMediaItem, newItem.mMediaItem)
+ && Objects.equals(oldItem.mText, newItem.mText);
+ }
+
+ @Override
+ public boolean areContentsTheSame(@NonNull BrowseViewData oldItem,
+ @NonNull BrowseViewData newItem) {
+ return oldItem.equals(newItem);
+ }
+ };
/**
* An {@link BrowseAdapter} observer.
*/
public static abstract class Observer {
- /**
- * Callback invoked anytime there is more information to be displayed, or if there is a
- * change in the overall state of the adapter.
- */
- protected void onDirty() {};
/**
* Callback invoked when a user clicks on a playable item.
*/
- protected void onPlayableItemClicked(MediaItemMetadata item) {};
+ protected void onPlayableItemClicked(MediaItemMetadata item) {
+ }
/**
* Callback invoked when a user clicks on a browsable item.
*/
- protected void onBrowseableItemClicked(MediaItemMetadata item) {};
-
- /**
- * Callback invoked when a user clicks on a the "more items" button on a section.
- */
- protected void onMoreButtonClicked(MediaItemMetadata item) {};
+ protected void onBrowsableItemClicked(MediaItemMetadata item) {
+ }
/**
* Callback invoked when the user clicks on the title of the queue.
*/
- protected void onQueueTitleClicked() {};
-
- /**
- * Callback invoked when the user clicks on a queue item.
- */
- protected void onQueueItemClicked(MediaItemMetadata item) {};
- }
-
- private MediaSource.ItemsSubscription mSubscriptionCallback =
- (mediaSource, parentId, items) -> {
- if (items != null) {
- onItemsLoaded(parentId, items);
- } else {
- onLoadingError(parentId);
- }
- };
-
-
- /**
- * Represents the loading state of children of a single {@link MediaItemMetadata} in the
- * {@link BrowseAdapter}
- */
- private class MediaItemState {
- /**
- * {@link com.android.car.media.common.MediaItemMetadata} whose children are being loaded
- */
- final MediaItemMetadata mItem;
- /** Current loading state for this item */
- State mState = State.LOADING;
- /** Playable children of the given item */
- List<MediaItemMetadata> mPlayableChildren = new ArrayList<>();
- /** Browsable children of the given item */
- List<MediaItemMetadata> mBrowsableChildren = new ArrayList<>();
- /** Whether we are subscribed to updates for this item or not */
- boolean mIsSubscribed;
-
- MediaItemState(MediaItemMetadata item) {
- mItem = item;
- }
-
- void setChildren(List<MediaItemMetadata> children) {
- mPlayableChildren.clear();
- mBrowsableChildren.clear();
- for (MediaItemMetadata child : children) {
- if (child.isBrowsable()) {
- // Browsable items could also be playable
- mBrowsableChildren.add(child);
- } else if (child.isPlayable()) {
- mPlayableChildren.add(child);
- }
- }
+ protected void onTitleClicked() {
}
}
/**
* Creates a {@link BrowseAdapter} that displays the children of the given media tree node.
- *
- * @param mediaSource the {@link MediaSource} to get data from.
- * @param parentItem the node to display children of, or NULL if the
- * @param strategy a {@link ContentForwardStrategy} that would determine which items would be
- * expanded and how.
*/
- public BrowseAdapter(Context context, @NonNull MediaSource mediaSource,
- @Nullable MediaItemMetadata parentItem, @NonNull ContentForwardStrategy strategy) {
+ public BrowseAdapter(@NonNull Context context) {
+ super(DIFF_CALLBACK);
mContext = context;
- mMediaSource = mediaSource;
- mParentMediaItem = parentItem;
- mCFBStrategy = strategy;
- }
-
- /**
- * Initiates or resumes the data loading process and subscribes to updates. The client can use
- * {@link #registerObserver(Observer)} to receive updates on the progress.
- */
- public void start() {
- mParentMediaItemId = mParentMediaItem != null ? mParentMediaItem.getId() :
- mMediaSource.getRoot();
- mMediaSource.subscribeChildren(mParentMediaItemId, mSubscriptionCallback);
- for (MediaItemState itemState : mItemStates.values()) {
- subscribe(itemState);
- }
}
/**
- * Stops the data loading and releases any subscriptions.
+ * Sets title to be displayed.
*/
- public void stop() {
- if (mParentMediaItemId == null) {
- // Not started
- return;
- }
- mMediaSource.unsubscribeChildren(mParentMediaItemId, mSubscriptionCallback);
- for (MediaItemState itemState : mItemStates.values()) {
- unsubscribe(itemState);
- }
- mParentMediaItemId = null;
- }
-
- /**
- * Replaces the media item whose children are being displayed in this adapter. The content of
- * the adapter will be replaced once the children of the new item are loaded.
- *
- * @param parentItem new media item to expand.
- */
- public void setParentMediaItemId(@Nullable MediaItemMetadata parentItem) {
- String newParentMediaItemId = parentItem != null ? parentItem.getId() :
- mMediaSource.getRoot();
- if (Objects.equals(newParentMediaItemId, mParentMediaItemId)) {
- return;
- }
- stop();
- mParentMediaItem = parentItem;
- mParentMediaItemId = newParentMediaItemId;
- mMediaSource.subscribeChildren(mParentMediaItemId, mSubscriptionCallback);
- }
-
- /**
- * Sets media queue items into this adapter.
- */
- public void setQueue(List<MediaItemMetadata> items, CharSequence queueTitle) {
- mQueue = items;
- mQueueTitle = queueTitle;
- notify(Observer::onDirty);
+ public void setTitle(CharSequence title) {
+ mTitle = title;
}
/**
@@ -265,15 +134,6 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> implem
}
/**
- * @return the global loading state. Consumers can use this state to determine if more
- * information is still pending to arrive or not. This method will report
- * {@link State#ERROR} only if the list of immediate children fails to load.
- */
- public State getState() {
- return mState;
- }
-
- /**
* Sets the number of columns that items can take. This method only needs to be used if the
* attached {@link RecyclerView} is NOT using a {@link GridLayoutManager}. This class will
* automatically determine this value on {@link #onAttachedToRecyclerView(RecyclerView)}
@@ -283,54 +143,30 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> implem
mMaxSpanSize = maxSpanSize;
}
+ public void setRootBrowsableViewType(int hintValue) {
+ mRootBrowsableViewType = fromMediaHint(hintValue);
+ }
+
+ public void setRootPlayableViewType(int hintValue) {
+ mRootPlayableViewType = fromMediaHint(hintValue);
+ }
+
/**
* @return a {@link GridLayoutManager.SpanSizeLookup} that can be used to obtain the span size
* of each item in this adapter. This method is only needed if the {@link RecyclerView} is NOT
- * using a {@link GridLayoutManager}. This class will automatically use it on\
- * {@link #onAttachedToRecyclerView(RecyclerView)} otherwise.
+ * using a {@link GridLayoutManager}. This class will automatically use it on\ {@link
+ * #onAttachedToRecyclerView(RecyclerView)} otherwise.
*/
- public GridLayoutManager.SpanSizeLookup getSpanSizeLookup() {
+ private GridLayoutManager.SpanSizeLookup getSpanSizeLookup() {
return new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
- BrowseItemViewType viewType = mViewData.get(position).mViewType;
+ BrowseItemViewType viewType = getItem(position).mViewType;
return viewType.getSpanSize(mMaxSpanSize);
}
};
}
- /**
- * Updates the {@link RecyclerView} with newly loaded information. This normally should be
- * invoked as a result of a {@link Observer#onDirty()} callback.
- *
- * This method is idempotent and can be used at any time (even delayed if needed). Additions,
- * removals and insertions would be notified to the {@link RecyclerView} so it can be
- * animated appropriately.
- */
- public void update() {
- List<BrowseViewData> newItems = generateViewData(mItemStates.values());
- List<BrowseViewData> oldItems = mViewData;
- mViewData = newItems;
- DiffUtil.DiffResult result = DiffUtil.calculateDiff(createDiffUtil(oldItems, newItems));
- result.dispatchUpdatesTo(this);
- }
-
- private void subscribe(MediaItemState state) {
- if (!state.mIsSubscribed && state.mItem.isBrowsable()) {
- mMediaSource.subscribeChildren(state.mItem.getId(), mSubscriptionCallback);
- state.mIsSubscribed = true;
- } else {
- state.mState = State.LOADED;
- }
- }
-
- private void unsubscribe(MediaItemState state) {
- if (state.mIsSubscribed) {
- mMediaSource.unsubscribeChildren(state.mItem.getId(), mSubscriptionCallback);
- state.mIsSubscribed = false;
- }
- }
-
@NonNull
@Override
public BrowseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
@@ -341,59 +177,23 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> implem
@Override
public void onBindViewHolder(@NonNull BrowseViewHolder holder, int position) {
- BrowseViewData viewData = mViewData.get(position);
+ BrowseViewData viewData = getItem(position);
holder.bind(mContext, viewData);
}
@Override
- public int getItemCount() {
- return mViewData.size();
- }
-
- @Override
public int getItemViewType(int position) {
- return mViewData.get(position).mViewType.ordinal();
+ return getItem(position).mViewType.ordinal();
}
- private void onItemsLoaded(String parentId, List<MediaItemMetadata> children) {
- if (parentId.equals(mParentMediaItemId)) {
- // Direct children from the requested media item id. Update subscription list.
- LinkedHashMap<String, MediaItemState> newItemStates = new LinkedHashMap<>();
- List<MediaItemState> itemsToSubscribe = new ArrayList<>();
- for (MediaItemMetadata item : children) {
- MediaItemState itemState = mItemStates.get(item.getId());
- if (itemState != null) {
- // Reuse existing section.
- newItemStates.put(item.getId(), itemState);
- mItemStates.remove(item.getId());
- } else {
- // New section, subscribe to it.
- itemState = new MediaItemState(item);
- newItemStates.put(item.getId(), itemState);
- itemsToSubscribe.add(itemState);
- }
- }
- // Remove unused sections
- for (MediaItemState itemState : mItemStates.values()) {
- unsubscribe(itemState);
- }
- mItemStates = newItemStates;
- // Subscribe items once we have updated the map (updates might happen synchronously
- // if data is already available).
- for (MediaItemState itemState : itemsToSubscribe) {
- subscribe(itemState);
- }
- } else {
- MediaItemState itemState = mItemStates.get(parentId);
- if (itemState == null) {
- Log.w(TAG, "Loaded children for a section we don't have: " + parentId);
- return;
- }
- itemState.setChildren(children);
- itemState.mState = State.LOADED;
+ public void submitItems(@Nullable MediaItemMetadata parentItem,
+ @Nullable List<MediaItemMetadata> children) {
+ mParentMediaItem = parentItem;
+ if (children == null) {
+ submitList(Collections.emptyList());
+ return;
}
- updateGlobalState();
- notify(Observer::onDirty);
+ submitList(generateViewData(children));
}
private void notify(Consumer<Observer> notification) {
@@ -402,66 +202,8 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> implem
}
}
- private void onLoadingError(String parentId) {
- if (parentId.equals(mParentMediaItemId)) {
- mState = State.ERROR;
- } else {
- MediaItemState state = mItemStates.get(parentId);
- if (state == null) {
- Log.w(TAG, "Error loading children for a section we don't have: " + parentId);
- return;
- }
- state.setChildren(new ArrayList<>());
- state.mState = State.ERROR;
- updateGlobalState();
- }
- notify(Observer::onDirty);
- }
-
- private void updateGlobalState() {
- for (MediaItemState state: mItemStates.values()) {
- if (state.mState == State.LOADING) {
- mState = State.LOADING;
- return;
- }
- }
- mState = State.LOADED;
- }
-
- private DiffUtil.Callback createDiffUtil(List<BrowseViewData> oldList,
- List<BrowseViewData> newList) {
- return new DiffUtil.Callback() {
- @Override
- public int getOldListSize() {
- return oldList.size();
- }
-
- @Override
- public int getNewListSize() {
- return newList.size();
- }
-
- @Override
- public boolean areItemsTheSame(int oldPos, int newPos) {
- BrowseViewData oldItem = oldList.get(oldPos);
- BrowseViewData newItem = newList.get(newPos);
-
- return Objects.equals(oldItem.mMediaItem, newItem.mMediaItem)
- && Objects.equals(oldItem.mText, newItem.mText);
- }
-
- @Override
- public boolean areContentsTheSame(int oldPos, int newPos) {
- BrowseViewData oldItem = oldList.get(oldPos);
- BrowseViewData newItem = newList.get(newPos);
-
- return oldItem.equals(newItem);
- }
- };
- }
-
@Override
- public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+ public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
GridLayoutManager manager = (GridLayoutManager) recyclerView.getLayoutManager();
mMaxSpanSize = manager.getSpanCount();
@@ -472,48 +214,26 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> implem
private class ItemsBuilder {
private List<BrowseViewData> result = new ArrayList<>();
- void addItem(MediaItemMetadata item, State state,
+ void addItem(MediaItemMetadata item,
BrowseItemViewType viewType, Consumer<Observer> notification) {
View.OnClickListener listener = notification != null ?
view -> BrowseAdapter.this.notify(notification) :
null;
- result.add(new BrowseViewData(item, viewType, state, listener));
- }
-
- void addItems(List<MediaItemMetadata> items, BrowseItemViewType viewType, int maxRows) {
- int spanSize = viewType.getSpanSize(mMaxSpanSize);
- int maxChildren = maxRows * (mMaxSpanSize / spanSize);
- result.addAll(items.stream()
- .limit(maxChildren)
- .map(item -> {
- Consumer<Observer> notification = item.getQueueId() != null
- ? observer -> observer.onQueueItemClicked(item)
- : item.isBrowsable()
- ? observer -> observer.onBrowseableItemClicked(item)
- : observer -> observer.onPlayableItemClicked(item);
- return new BrowseViewData(item, viewType, null, view ->
- BrowseAdapter.this.notify(notification));
- })
- .collect(Collectors.toList()));
+ result.add(new BrowseViewData(item, viewType, listener));
}
void addTitle(CharSequence title, Consumer<Observer> notification) {
- result.add(new BrowseViewData(title, BrowseItemViewType.HEADER,
- view -> BrowseAdapter.this.notify(notification)));
-
+ if (title == null) {
+ title = "";
+ }
+ View.OnClickListener listener = notification != null ?
+ view -> BrowseAdapter.this.notify(notification) :
+ null;
+ result.add(new BrowseViewData(title, BrowseItemViewType.HEADER, listener));
}
- void addBrowseBlock(MediaItemMetadata header, State state,
- List<MediaItemMetadata> items, BrowseItemViewType viewType, int maxChildren,
- boolean showHeader, boolean showMoreFooter) {
- if (showHeader) {
- addItem(header, state, BrowseItemViewType.HEADER, null);
- }
- addItems(items, viewType, maxChildren);
- if (showMoreFooter) {
- addItem(header, null, BrowseItemViewType.MORE_FOOTER,
- observer -> observer.onMoreButtonClicked(header));
- }
+ void addSpacer() {
+ result.add(new BrowseViewData(BrowseItemViewType.SPACER, null));
}
List<BrowseViewData> build() {
@@ -526,83 +246,36 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> implem
* flickering, the flatting will stop at the first "loading" section, avoiding unnecessary
* insertion animations during the initial data load.
*/
- private List<BrowseViewData> generateViewData(Collection<MediaItemState> itemStates) {
+ private List<BrowseViewData> generateViewData(List<MediaItemMetadata> items) {
ItemsBuilder itemsBuilder = new ItemsBuilder();
-
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Generating browse view from:");
- for (MediaItemState item : itemStates) {
+ for (MediaItemMetadata item : items) {
Log.v(TAG, String.format("[%s%s] '%s' (%s)",
- item.mItem.isBrowsable() ? "B" : " ",
- item.mItem.isPlayable() ? "P" : " ",
- item.mItem.getTitle(),
- item.mItem.getId()));
- List<MediaItemMetadata> items = new ArrayList<>();
- items.addAll(item.mBrowsableChildren);
- items.addAll(item.mPlayableChildren);
- for (MediaItemMetadata child : items) {
- Log.v(TAG, String.format(" [%s%s] '%s' (%s)",
- child.isBrowsable() ? "B" : " ",
- child.isPlayable() ? "P" : " ",
- child.getTitle(),
- child.getId()));
- }
+ item.isBrowsable() ? "B" : " ",
+ item.isPlayable() ? "P" : " ",
+ item.getTitle(),
+ item.getId()));
}
}
- if (mQueue != null && !mQueue.isEmpty() && mCFBStrategy.getMaxQueueRows() > 0
- && mCFBStrategy.getQueueViewType() != null) {
- if (mQueueTitle != null) {
- itemsBuilder.addTitle(mQueueTitle, Observer::onQueueTitleClicked);
- }
- itemsBuilder.addItems(mQueue, mCFBStrategy.getQueueViewType(),
- mCFBStrategy.getMaxQueueRows());
+ if (mTitle != null) {
+ itemsBuilder.addTitle(mTitle, Observer::onTitleClicked);
+ } else if (!items.isEmpty() && items.get(0).getTitleGrouping() == null) {
+ itemsBuilder.addSpacer();
}
-
- boolean containsBrowsableItems = false;
- boolean containsPlayableItems = false;
- for (MediaItemState itemState : itemStates) {
- containsBrowsableItems |= itemState.mItem.isBrowsable();
- containsPlayableItems |= itemState.mItem.isPlayable();
- }
-
- for (MediaItemState itemState : itemStates) {
- MediaItemMetadata item = itemState.mItem;
- if (containsPlayableItems && containsBrowsableItems) {
- // If we have a mix of browsable and playable items: show them all in a list
- itemsBuilder.addItem(item, itemState.mState,
- BrowseItemViewType.PANEL_ITEM,
- item.isBrowsable()
- ? observer -> observer.onBrowseableItemClicked(item)
- : observer -> observer.onPlayableItemClicked(item));
- } else if (itemState.mItem.isBrowsable()) {
- // If we only have browsable items, check whether we should expand them or not.
- if (!itemState.mBrowsableChildren.isEmpty()
- && !itemState.mPlayableChildren.isEmpty()
- || !mCFBStrategy.shouldBeExpanded(item)) {
- itemsBuilder.addItem(item, itemState.mState,
- mCFBStrategy.getBrowsableViewType(mParentMediaItem), null);
- } else if (!itemState.mPlayableChildren.isEmpty()) {
- itemsBuilder.addBrowseBlock(item,
- itemState.mState,
- itemState.mPlayableChildren,
- mCFBStrategy.getPlayableViewType(item),
- mCFBStrategy.getMaxRows(item, mCFBStrategy.getPlayableViewType(item)),
- mCFBStrategy.includeHeader(item),
- mCFBStrategy.showMoreButton(item));
- } else if (!itemState.mBrowsableChildren.isEmpty()) {
- itemsBuilder.addBrowseBlock(item,
- itemState.mState,
- itemState.mBrowsableChildren,
- mCFBStrategy.getBrowsableViewType(item),
- mCFBStrategy.getMaxRows(item, mCFBStrategy.getBrowsableViewType(item)),
- mCFBStrategy.includeHeader(item),
- mCFBStrategy.showMoreButton(item));
- }
+ String currentTitleGrouping = null;
+ for (MediaItemMetadata item : items) {
+ String titleGrouping = item.getTitleGrouping();
+ if (!Objects.equals(currentTitleGrouping, titleGrouping)) {
+ currentTitleGrouping = titleGrouping;
+ itemsBuilder.addTitle(titleGrouping, null);
+ }
+ if (item.isBrowsable()) {
+ itemsBuilder.addItem(item, getBrowsableViewType(mParentMediaItem),
+ observer -> observer.onBrowsableItemClicked(item));
} else if (item.isPlayable()) {
- // If we only have playable items: show them as so.
- itemsBuilder.addItem(item, itemState.mState,
- mCFBStrategy.getPlayableViewType(mParentMediaItem),
+ itemsBuilder.addItem(item, getPlayableViewType(mParentMediaItem),
observer -> observer.onPlayableItemClicked(item));
}
}
@@ -610,11 +283,38 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> implem
return itemsBuilder.build();
}
- @Override
- public boolean shouldHideDivider(int position) {
- return position >= mViewData.size() - 1
- || position < 0
- || mViewData.get(position).mViewType != BrowseItemViewType.PANEL_ITEM
- || mViewData.get(position + 1).mViewType != BrowseItemViewType.PANEL_ITEM;
+ private BrowseItemViewType getBrowsableViewType(@Nullable MediaItemMetadata mediaItem) {
+ if (mediaItem == null) {
+ return BrowseItemViewType.LIST_ITEM;
+ }
+ if (mediaItem.getBrowsableContentStyleHint() == 0) {
+ return mRootBrowsableViewType;
+ }
+ return fromMediaHint(mediaItem.getBrowsableContentStyleHint());
+ }
+
+ private BrowseItemViewType getPlayableViewType(@Nullable MediaItemMetadata mediaItem) {
+ if (mediaItem == null) {
+ return BrowseItemViewType.LIST_ITEM;
+ }
+ if (mediaItem.getPlayableContentStyleHint() == 0) {
+ return mRootPlayableViewType;
+ }
+ return fromMediaHint(mediaItem.getPlayableContentStyleHint());
+ }
+
+ /**
+ * Converts a content style hint to the appropriate {@link BrowseItemViewType}, defaulting to
+ * list items.
+ */
+ private BrowseItemViewType fromMediaHint(int hint) {
+ switch(hint) {
+ case MediaConstants.CONTENT_STYLE_GRID_ITEM_HINT_VALUE:
+ return BrowseItemViewType.GRID_ITEM;
+ case MediaConstants.CONTENT_STYLE_LIST_ITEM_HINT_VALUE:
+ return BrowseItemViewType.LIST_ITEM;
+ default:
+ return BrowseItemViewType.LIST_ITEM;
+ }
}
}
diff --git a/src/com/android/car/media/browse/BrowseItemViewType.java b/src/com/android/car/media/browse/BrowseItemViewType.java
index 63a54fe..f4dca73 100644
--- a/src/com/android/car/media/browse/BrowseItemViewType.java
+++ b/src/com/android/car/media/browse/BrowseItemViewType.java
@@ -28,17 +28,16 @@ public enum BrowseItemViewType {
GRID_ITEM(com.android.car.media.R.layout.media_browse_grid_item, 1),
/** A list item including title and subtitle */
LIST_ITEM(com.android.car.media.R.layout.media_browse_list_item),
- /** An item in a panel of items (menu) */
- PANEL_ITEM(com.android.car.media.R.layout.media_browse_panel_item),
- /** A footer that can be used to navigate to an expanded version of a section */
- MORE_FOOTER(com.android.car.media.R.layout.media_browse_more_footer),
- ;
- private final @LayoutRes int mLayoutId;
+ /** A spacer view that creates additional padding at the edges of the list, and for headers */
+ SPACER(com.android.car.media.R.layout.media_browse_spacer);
+
+ @LayoutRes
+ private final int mLayoutId;
private final int mSpanSize;
/**
- * {@link BrowseItemViewType} that take the whole width of the
- * {@link androidx.recyclerview.widget.RecyclerView}
+ * {@link BrowseItemViewType} that take the whole width of the {@link
+ * androidx.recyclerview.widget.RecyclerView}
*/
BrowseItemViewType(@LayoutRes int layoutId) {
mLayoutId = layoutId;
@@ -62,9 +61,10 @@ public enum BrowseItemViewType {
}
/**
- * @return layout that should be inflated to generate this view type.
+ * Returns the layout that should be inflated to generate this view type.
*/
- public @LayoutRes int getLayoutId() {
+ @LayoutRes
+ public int getLayoutId() {
return mLayoutId;
}
}
diff --git a/src/com/android/car/media/browse/BrowseViewData.java b/src/com/android/car/media/browse/BrowseViewData.java
index f9c2ced..c200f7d 100644
--- a/src/com/android/car/media/browse/BrowseViewData.java
+++ b/src/com/android/car/media/browse/BrowseViewData.java
@@ -31,9 +31,8 @@ class BrowseViewData {
/** {@link com.android.car.media.common.MediaItemMetadata} associated with this item */
public final MediaItemMetadata mMediaItem;
/** View type associated with this item */
- @NonNull public final BrowseItemViewType mViewType;
- /** Current state of this item */
- public final BrowseAdapter.State mState;
+ @NonNull
+ public final BrowseItemViewType mViewType;
/** Text associated with this item */
public final CharSequence mText;
/** Click listener to set for this item */
@@ -42,24 +41,23 @@ class BrowseViewData {
/**
* Creates a {@link BrowseViewData} for a particular {@link MediaItemMetadata}.
*
- * @param mediaItem {@link MediaItemMetadata} metadata
- * @param viewType view type to use to represent this item
- * @param state current item state
+ * @param mediaItem {@link MediaItemMetadata} metadata
+ * @param viewType view type to use to represent this item
* @param onClickListener optional {@link android.view.View.OnClickListener}
*/
BrowseViewData(MediaItemMetadata mediaItem, @NonNull BrowseItemViewType viewType,
- @NonNull BrowseAdapter.State state, View.OnClickListener onClickListener) {
+ View.OnClickListener onClickListener) {
mMediaItem = mediaItem;
mViewType = viewType;
- mState = state;
mText = null;
mOnClickListener = onClickListener;
}
/**
* Creates a {@link BrowseViewData} for a given text (normally used for headers or footers)
- * @param text text to set
- * @param viewType view type to use
+ *
+ * @param text text to set
+ * @param viewType view type to use
* @param onClickListener optional {@link android.view.View.OnClickListener}
*/
BrowseViewData(@NonNull CharSequence text, @NonNull BrowseItemViewType viewType,
@@ -67,7 +65,16 @@ class BrowseViewData {
mText = text;
mViewType = viewType;
mMediaItem = null;
- mState = null;
+ mOnClickListener = onClickListener;
+ }
+
+ /**
+ * Creates a {@link BrowseViewData} with no metadata
+ */
+ BrowseViewData(@NonNull BrowseItemViewType viewType, View.OnClickListener onClickListener) {
+ mText = null;
+ mMediaItem = null;
+ mViewType = viewType;
mOnClickListener = onClickListener;
}
@@ -77,12 +84,11 @@ class BrowseViewData {
if (o == null || getClass() != o.getClass()) return false;
BrowseViewData item = (BrowseViewData) o;
return Objects.equals(mMediaItem, item.mMediaItem) &&
- mState == item.mState &&
mViewType == item.mViewType;
}
@Override
public int hashCode() {
- return Objects.hash(mMediaItem, mState, mViewType);
+ return Objects.hash(mMediaItem, mViewType);
}
}
diff --git a/src/com/android/car/media/browse/BrowseViewHolder.java b/src/com/android/car/media/browse/BrowseViewHolder.java
index 39606cf..38106f5 100644
--- a/src/com/android/car/media/browse/BrowseViewHolder.java
+++ b/src/com/android/car/media/browse/BrowseViewHolder.java
@@ -17,6 +17,7 @@
package com.android.car.media.browse;
import android.content.Context;
+import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
@@ -24,6 +25,7 @@ import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
+import com.android.car.apps.common.util.ViewUtils;
import com.android.car.media.common.MediaItemMetadata;
/**
@@ -34,6 +36,11 @@ class BrowseViewHolder extends RecyclerView.ViewHolder {
final TextView mSubtitle;
final ImageView mAlbumArt;
final ViewGroup mContainer;
+ final ImageView mRightArrow;
+ final ImageView mTitleDownloadIcon;
+ final ImageView mTitleExplicitIcon;
+ final ImageView mSubTitleDownloadIcon;
+ final ImageView mSubTitleExplicitIcon;
/**
* Creates a {@link BrowseViewHolder} for the given view.
@@ -44,32 +51,48 @@ class BrowseViewHolder extends RecyclerView.ViewHolder {
mSubtitle = itemView.findViewById(com.android.car.media.R.id.subtitle);
mAlbumArt = itemView.findViewById(com.android.car.media.R.id.thumbnail);
mContainer = itemView.findViewById(com.android.car.media.R.id.container);
+ mRightArrow = itemView.findViewById(com.android.car.media.R.id.right_arrow);
+ mTitleDownloadIcon = itemView.findViewById(
+ com.android.car.media.R.id.download_icon_with_title);
+ mTitleExplicitIcon = itemView.findViewById(
+ com.android.car.media.R.id.explicit_icon_with_title);
+ mSubTitleDownloadIcon = itemView.findViewById(
+ com.android.car.media.R.id.download_icon_with_subtitle);
+ mSubTitleExplicitIcon = itemView.findViewById(
+ com.android.car.media.R.id.explicit_icon_with_subtitle);
}
/**
* Updates this {@link BrowseViewHolder} with the given data
*/
public void bind(Context context, BrowseViewData data) {
+
+ boolean hasMediaItem = data.mMediaItem != null;
+ boolean showSubtitle = hasMediaItem && !TextUtils.isEmpty(data.mMediaItem.getSubtitle());
+
if (mTitle != null) {
- mTitle.setText(data.mText != null
- ? data.mText : data.mMediaItem != null
- ? data.mMediaItem.getTitle()
- : null);
+ mTitle.setText(data.mText != null ? data.mText :
+ hasMediaItem ? data.mMediaItem.getTitle() : null);
}
if (mSubtitle != null) {
- mSubtitle.setText(data.mMediaItem != null
- ? data.mMediaItem.getSubtitle()
- : null);
- mSubtitle.setVisibility(data.mMediaItem != null && data.mMediaItem.getSubtitle() != null
- && !data.mMediaItem.getSubtitle().toString().isEmpty()
- ? View.VISIBLE
- : View.GONE);
+ mSubtitle.setText(hasMediaItem ? data.mMediaItem.getSubtitle() : null);
+ ViewUtils.setVisible(mSubtitle, showSubtitle);
}
if (mAlbumArt != null) {
- MediaItemMetadata.updateImageView(context, data.mMediaItem, mAlbumArt, 0);
+ MediaItemMetadata.updateImageView(context, data.mMediaItem, mAlbumArt, 0, true);
}
if (mContainer != null && data.mOnClickListener != null) {
mContainer.setOnClickListener(data.mOnClickListener);
}
+ ViewUtils.setVisible(mRightArrow, hasMediaItem && data.mMediaItem.isBrowsable());
+
+ // Adjust the positioning of the explicit and downloaded icons. If there is a subtitle, then
+ // the icons should show on the subtitle row, otherwise they should show on the title row.
+ boolean downloaded = hasMediaItem && data.mMediaItem.isDownloaded();
+ boolean explicit = hasMediaItem && data.mMediaItem.isExplicit();
+ ViewUtils.setVisible(mTitleDownloadIcon, !showSubtitle && downloaded);
+ ViewUtils.setVisible(mTitleExplicitIcon, !showSubtitle && explicit);
+ ViewUtils.setVisible(mSubTitleDownloadIcon, showSubtitle && downloaded);
+ ViewUtils.setVisible(mSubTitleExplicitIcon, showSubtitle && explicit);
}
}
diff --git a/src/com/android/car/media/browse/ContentForwardStrategy.java b/src/com/android/car/media/browse/ContentForwardStrategy.java
deleted file mode 100644
index b9e2fba..0000000
--- a/src/com/android/car/media/browse/ContentForwardStrategy.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.media.browse;
-
-import android.os.Bundle;
-import com.android.car.media.common.ContentStyleMediaConstants;
-import com.android.car.media.common.MediaItemMetadata;
-
-/**
- * Strategy used to group and expand media items in the {@link BrowseAdapter}
- */
-public interface ContentForwardStrategy {
- /**
- * @return true if a header should be included when expanding the given media item into a
- * section. Only used if {@link #shouldBeExpanded(MediaItemMetadata)} returns true.
- */
- boolean includeHeader(MediaItemMetadata mediaItem);
-
- /**
- * @return maximum number of rows to use when when expanding the given media item into a
- * section. The number can be different depending on the {@link BrowseItemViewType} that
- * will be used to represent media item children (i.e.: we might allow more rows for lists
- * than for grids). Only used if {@link #shouldBeExpanded(MediaItemMetadata)} returns true.
- */
- int getMaxRows(MediaItemMetadata mediaItem, BrowseItemViewType viewType);
-
- /**
- * @return whether the given media item should be expanded or not. If not expanded, the item
- * will be displayed according to its parent preferred view type.
- */
- boolean shouldBeExpanded(MediaItemMetadata mediaItem);
-
- /**
- * @return view type to use to render browsable children of the given media item. Only used if
- * {@link #shouldBeExpanded(MediaItemMetadata)} returns true.
- */
- BrowseItemViewType getBrowsableViewType(MediaItemMetadata mediaItem);
-
- /**
- * @return view type to use to render playable children fo the given media item. Only used if
- * {@link #shouldBeExpanded(MediaItemMetadata)} returns true.
- */
- BrowseItemViewType getPlayableViewType(MediaItemMetadata mediaItem);
-
- /**
- * @return true if a "more" button should be displayed as a footer for a section displaying the
- * given media item, in case that there item has more children than the ones that can be
- * displayed according to {@link #getMaxQueueRows()}. Only used if
- * {@link #shouldBeExpanded(MediaItemMetadata)} returns true.
- */
- boolean showMoreButton(MediaItemMetadata mediaItem);
-
- /**
- * @return maximum number of items to show for the media queue, if one is provided.
- */
- int getMaxQueueRows();
-
- /**
- * @return view type to use to display queue items.
- */
- BrowseItemViewType getQueueViewType();
-
- /**
- * Default strategy
- * TODO(b/77646944): Expand this implementation to honor the media source expectations.
- */
- ContentForwardStrategy DEFAULT_STRATEGY = new ContentForwardStrategy() {
- @Override
- public boolean includeHeader(MediaItemMetadata mediaItem) {
- return true;
- }
-
- @Override
- public int getMaxRows(MediaItemMetadata mediaItem, BrowseItemViewType viewType) {
- return viewType == BrowseItemViewType.GRID_ITEM ? 2 : 8;
- }
-
- @Override
- public boolean shouldBeExpanded(MediaItemMetadata mediaItem) {
- return true;
- }
-
- @Override
- public BrowseItemViewType getBrowsableViewType(MediaItemMetadata mediaItem) {
- return (mediaItem.getBrowsableContentStyleHint()
- == ContentStyleMediaConstants.CONTENT_STYLE_LIST_ITEM_HINT_VALUE)
- ? BrowseItemViewType.LIST_ITEM
- : BrowseItemViewType.PANEL_ITEM;
- }
-
- @Override
- public BrowseItemViewType getPlayableViewType(MediaItemMetadata mediaItem) {
- return (mediaItem.getPlayableContentStyleHint()
- == ContentStyleMediaConstants.CONTENT_STYLE_LIST_ITEM_HINT_VALUE)
- ? BrowseItemViewType.LIST_ITEM
- : BrowseItemViewType.GRID_ITEM;
- }
-
- @Override
- public boolean showMoreButton(MediaItemMetadata mediaItem) {
- return false;
- }
-
- @Override
- public int getMaxQueueRows() {
- return 8;
- }
-
- @Override
- public BrowseItemViewType getQueueViewType() {
- return BrowseItemViewType.LIST_ITEM;
- }
- };
-}
diff --git a/src/com/android/car/media/drawer/MediaBrowserItemsFetcher.java b/src/com/android/car/media/drawer/MediaBrowserItemsFetcher.java
deleted file mode 100644
index eb094fc..0000000
--- a/src/com/android/car/media/drawer/MediaBrowserItemsFetcher.java
+++ /dev/null
@@ -1,199 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.media.drawer;
-
-import android.content.Context;
-import android.graphics.PorterDuff;
-import android.graphics.drawable.Drawable;
-import android.media.MediaDescription;
-import android.media.browse.MediaBrowser;
-import android.media.session.MediaSession;
-import android.util.Log;
-
-import androidx.car.drawer.DrawerItemViewHolder;
-
-import com.android.car.media.MediaPlaybackModel;
-import com.android.car.media.R;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * {@link MediaItemsFetcher} implementation that fetches items from a specific {@link MediaBrowser}
- * node.
- * <p>
- * It optionally supports surfacing the Media app's queue as the last item.
- */
-class MediaBrowserItemsFetcher implements MediaItemsFetcher {
- private static final String TAG = "Media.BrowserFetcher";
-
- /**
- * An id that can be returned from {@link MediaBrowser.MediaItem#getMediaId()} to indicate that
- * a {@link android.media.browse.MediaBrowser.MediaItem} representing the play queue has been
- * clicked.
- */
- static final String PLAY_QUEUE_MEDIA_ID = "com.android.car.media.drawer.PLAY_QUEUE";
-
- private final Context mContext;
- private final MediaPlaybackModel mMediaPlaybackModel;
- private final String mMediaId;
- private final boolean mShowQueueItem;
- private final MediaItemOnClickListener mItemClickListener;
- private ItemsUpdatedCallback mCallback;
- private List<MediaBrowser.MediaItem> mItems = new ArrayList<>();
- private boolean mQueueAvailable;
-
- MediaBrowserItemsFetcher(Context context, MediaPlaybackModel model,
- MediaItemOnClickListener listener, String mediaId, boolean showQueueItem) {
- mContext = context;
- mMediaPlaybackModel = model;
- mItemClickListener = listener;
- mMediaId = mediaId;
- mShowQueueItem = showQueueItem;
- }
-
- @Override
- public void start(ItemsUpdatedCallback callback) {
- mCallback = callback;
- updateQueueAvailability();
- if (mMediaPlaybackModel.isConnected()) {
- mMediaPlaybackModel.getMediaBrowser().subscribe(mMediaId, mSubscriptionCallback);
- } else {
- mItems.clear();
- callback.onItemsUpdated();
- }
- mMediaPlaybackModel.addListener(mModelListener);
- }
-
- private final MediaPlaybackModel.Listener mModelListener =
- new MediaPlaybackModel.AbstractListener() {
- @Override
- public void onQueueChanged(List<MediaSession.QueueItem> queue) {
- updateQueueAvailability();
- }
- @Override
- public void onSessionDestroyed(CharSequence destroyedMediaClientName) {
- updateQueueAvailability();
- }
- @Override
- public void onMediaConnectionSuspended() {
- if (mCallback != null) {
- mCallback.onItemsUpdated();
- }
- }
- };
-
- private final MediaBrowser.SubscriptionCallback mSubscriptionCallback =
- new MediaBrowser.SubscriptionCallback() {
- @Override
- public void onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children) {
- mItems.clear();
- mItems.addAll(children);
- mCallback.onItemsUpdated();
- }
-
- @Override
- public void onError(String parentId) {
- mItems.clear();
- mCallback.onItemsUpdated();
- }
- };
-
- private void updateQueueAvailability() {
- if (mShowQueueItem && !mMediaPlaybackModel.getQueue().isEmpty()) {
- mQueueAvailable = true;
- }
- }
-
- @Override
- public int getItemCount() {
- int size = mItems.size();
- if (mQueueAvailable) {
- size++;
- }
- return size;
- }
-
- @Override
- public boolean usesSmallLayout(int position) {
- if (mQueueAvailable && position == mItems.size()) {
- return true;
- }
- return MediaItemsFetcher.usesSmallLayout(mItems.get(position).getDescription());
- }
-
- @Override
- public void populateViewHolder(DrawerItemViewHolder holder, int position) {
- if (mQueueAvailable && position == mItems.size()) {
- holder.getTitle().setText(mMediaPlaybackModel.getQueueTitle());
- return;
- }
- MediaBrowser.MediaItem item = mItems.get(position);
- MediaItemsFetcher.populateViewHolderFrom(holder, item.getDescription());
-
- if (holder.getEndIcon() == null) {
- return;
- }
-
- if (item.isBrowsable()) {
- int iconColor = mContext.getColor(R.color.car_tint);
- Drawable drawable = mContext.getDrawable(R.drawable.ic_chevron_right);
- drawable.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN);
- holder.getEndIcon().setImageDrawable(drawable);
- } else {
- holder.getEndIcon().setImageDrawable(null);
- }
- }
-
- @Override
- public void onItemClick(int position) {
- if (mItemClickListener == null) {
- return;
- }
-
- MediaBrowser.MediaItem item = mQueueAvailable && position == mItems.size()
- ? createPlayQueueMediaItem()
- : mItems.get(position);
-
- mItemClickListener.onMediaItemClicked(item);
- }
-
- /**
- * Creates and returns a {@link android.media.browse.MediaBrowser.MediaItem} that represents an
- * entry for the play queue. A play queue media item will have a media id of
- * {@link #PLAY_QUEUE_MEDIA_ID} and is {@link MediaBrowser.MediaItem#FLAG_BROWSABLE}.
- */
- private MediaBrowser.MediaItem createPlayQueueMediaItem() {
- MediaDescription description = new MediaDescription.Builder()
- .setMediaId(PLAY_QUEUE_MEDIA_ID)
- .setTitle(mMediaPlaybackModel.getQueueTitle())
- .build();
-
- return new MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_BROWSABLE);
- }
-
- @Override
- public void cleanup() {
- mMediaPlaybackModel.removeListener(mModelListener);
- mMediaPlaybackModel.getMediaBrowser().unsubscribe(mMediaId);
- mCallback = null;
- }
-
- @Override
- public int getScrollPosition() {
- return MediaItemsFetcher.DONT_SCROLL;
- }
-}
diff --git a/src/com/android/car/media/drawer/MediaDrawerAdapter.java b/src/com/android/car/media/drawer/MediaDrawerAdapter.java
deleted file mode 100644
index e1cf1b6..0000000
--- a/src/com/android/car/media/drawer/MediaDrawerAdapter.java
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.media.drawer;
-
-import android.content.Context;
-
-import androidx.annotation.Nullable;
-import androidx.car.drawer.CarDrawerAdapter;
-import androidx.car.drawer.CarDrawerController;
-import androidx.car.drawer.DrawerItemViewHolder;
-import androidx.recyclerview.widget.RecyclerView;
-
-/**
- * Subclass of CarDrawerAdapter used by the Media app.
- * <p>
- * This adapter delegates actual fetching of items (and other operations) to a
- * {@link MediaItemsFetcher}. The current fetcher being used can be updated at runtime.
- */
-class MediaDrawerAdapter extends CarDrawerAdapter {
- private final CarDrawerController mDrawerController;
- private MediaItemsFetcher mCurrentFetcher;
- private MediaFetchCallback mFetchCallback;
- private int mCurrentScrollPosition;
-
- /**
- * Interface for a callback object that will be notified of changes to the fetch status of
- * items in a media drawer.
- */
- interface MediaFetchCallback {
- /**
- * Called when a fetch for items starts.
- */
- void onFetchStart();
-
- /**
- * Called when a fetch for items ends.
- */
- void onFetchEnd();
- }
-
- MediaDrawerAdapter(Context context, CarDrawerController drawerController) {
- super(context, true /* showDisabledListOnEmpty */);
- mDrawerController = drawerController;
- }
-
- /**
- * Sets the object to be notified of changes to the fetching of items in the media drawer.
- */
- void setFetchCallback(@Nullable MediaFetchCallback callback) {
- mFetchCallback = callback;
- }
-
- /**
- * Switch the {@link MediaItemsFetcher} being used to fetch items. The new fetcher is kicked-off
- * and the drawer's content's will be updated to show newly loaded items. Any old fetcher is
- * cleaned up and released.
- *
- * @param fetcher New {@link MediaItemsFetcher} to use for display Drawer items.
- */
- void setFetcherAndInvoke(MediaItemsFetcher fetcher) {
- setFetcher(fetcher);
-
- if (mFetchCallback != null) {
- mFetchCallback.onFetchStart();
- }
-
- mCurrentFetcher.start(() -> {
- closeFetch();
- notifyDataSetChanged();
- });
- }
-
- void setFetcher(MediaItemsFetcher fetcher) {
- if (mCurrentFetcher != null) {
- mCurrentFetcher.cleanup();
- }
- mCurrentFetcher = fetcher;
- notifyDataSetChanged();
- }
-
- @Override
- protected int getActualItemCount() {
- return mCurrentFetcher != null ? mCurrentFetcher.getItemCount() : 0;
- }
-
- @Override
- protected boolean usesSmallLayout(int position) {
- return mCurrentFetcher.usesSmallLayout(position);
- }
-
- @Override
- protected void populateViewHolder(DrawerItemViewHolder holder, int position) {
- if (mCurrentFetcher == null) {
- return;
- }
-
- mCurrentFetcher.populateViewHolder(holder, position);
- scrollToCurrent();
- }
-
- @Override
- public void onItemClick(int position) {
- if (mCurrentFetcher != null) {
- mCurrentFetcher.onItemClick(position);
- }
- }
-
- @Override
- public void cleanup() {
- super.cleanup();
- if (mCurrentFetcher != null) {
- mCurrentFetcher.cleanup();
- mCurrentFetcher = null;
- notifyDataSetChanged();
- }
- closeFetch();
- }
-
- private void closeFetch() {
- if (mFetchCallback != null) {
- mFetchCallback.onFetchEnd();
- mFetchCallback = null;
- }
- }
-
- public void scrollToCurrent() {
- if (mCurrentFetcher == null) {
- return;
- }
- int scrollPosition = mCurrentFetcher.getScrollPosition();
- if (scrollPosition != MediaItemsFetcher.DONT_SCROLL
- && mCurrentScrollPosition != scrollPosition) {
- mDrawerController.scrollToPosition(scrollPosition);
- mCurrentScrollPosition = scrollPosition;
- }
- }
-
- @Override
- public void onAttachedToRecyclerView(RecyclerView recyclerView) {
- if (mCurrentFetcher != null) {
- MediaItemsFetcher fetcher = mCurrentFetcher;
- fetcher.cleanup();
- setFetcherAndInvoke(fetcher);
- }
- }
-
-}
diff --git a/src/com/android/car/media/drawer/MediaDrawerController.java b/src/com/android/car/media/drawer/MediaDrawerController.java
deleted file mode 100644
index 407bbe3..0000000
--- a/src/com/android/car/media/drawer/MediaDrawerController.java
+++ /dev/null
@@ -1,227 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.media.drawer;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.media.browse.MediaBrowser;
-import android.media.session.MediaController;
-import android.media.session.MediaSession;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.View;
-
-import androidx.annotation.Nullable;
-import androidx.car.drawer.CarDrawerAdapter;
-import androidx.car.drawer.CarDrawerController;
-import androidx.drawerlayout.widget.DrawerLayout;
-
-import com.android.car.media.MediaManager;
-import com.android.car.media.MediaPlaybackModel;
-import com.android.car.media.R;
-
-/**
- * Manages drawer navigation and item selection.
- * <p>
- * Maintains separate MediaPlaybackModel for media browsing and control. Sets up root Drawer
- * adapter with root of media-browse tree (using MediaBrowserItemsFetcher). Supports switching the
- * rootAdapter to show the queue-items (using MediaQueueItemsFetcher).
- */
-public class MediaDrawerController implements MediaDrawerAdapter.MediaFetchCallback,
- MediaItemOnClickListener {
- private static final String TAG = "MediaDrawerController";
-
- private static final String EXTRA_ICON_SIZE =
- "com.google.android.gms.car.media.BrowserIconSize";
-
- private final Context mContext;
- private final CarDrawerController mDrawerController;
- private final MediaPlaybackModel mMediaPlaybackModel;
- private MediaDrawerAdapter mRootAdapter;
-
- public MediaDrawerController(Context context, CarDrawerController drawerController) {
- mContext = context;
- mDrawerController = drawerController;
-
- Bundle extras = new Bundle();
- extras.putInt(EXTRA_ICON_SIZE,
- mContext.getResources().getDimensionPixelSize(R.dimen.car_primary_icon_size));
-
- mMediaPlaybackModel = new MediaPlaybackModel(mContext, extras);
- mMediaPlaybackModel.addListener(mModelListener);
-
- mRootAdapter = new MediaDrawerAdapter(mContext, mDrawerController);
- // Start with a empty title since we depend on the mMediaManagerListener callback to
- // know which app is being used and set the actual title there.
- mRootAdapter.setTitle("");
- mRootAdapter.setFetchCallback(this);
-
- // Kick off MediaBrowser/MediaController connection.
- mMediaPlaybackModel.start();
- }
-
- @Override
- public void onQueueItemClicked(MediaSession.QueueItem queueItem) {
- MediaController.TransportControls controls = mMediaPlaybackModel.getTransportControls();
-
- if (controls != null) {
- controls.skipToQueueItem(queueItem.getQueueId());
- }
-
- mDrawerController.closeDrawer();
- }
-
- @Override
- public void onMediaItemClicked(MediaBrowser.MediaItem item) {
- if (item.isBrowsable()) {
- MediaItemsFetcher fetcher;
- if (MediaBrowserItemsFetcher.PLAY_QUEUE_MEDIA_ID.equals(item.getMediaId())) {
- fetcher = createMediaQueueItemsFetcher();
- } else {
- fetcher = createMediaBrowserItemFetcher(item.getMediaId(),
- false /* showQueueItem */);
- }
- setupAdapterAndSwitch(fetcher, item.getDescription().getTitle());
- } else if (item.isPlayable()) {
- MediaController.TransportControls controls = mMediaPlaybackModel.getTransportControls();
- if (controls != null) {
- controls.playFromMediaId(item.getMediaId(), item.getDescription().getExtras());
- }
- mDrawerController.closeDrawer();
- } else {
- Log.w(TAG, "Unknown item type; don't know how to handle!");
- }
- }
-
- @Override
- public void onFetchStart() {
- // Initially there will be no items and we don't want to show empty-list indicator
- // briefly until items are fetched.
- mDrawerController.showLoadingProgressBar(true);
- }
-
- @Override
- public void onFetchEnd() {
- mDrawerController.showLoadingProgressBar(false);
- }
-
- /**
- * Creates a new sub-level in the drawer and switches to that as the currently displayed view.
- *
- * @param fetcher The {@link MediaItemsFetcher} that is responsible for fetching the items to be
- * displayed in the new view.
- * @param title The title text of the new view in the drawer.
- */
- private void setupAdapterAndSwitch(MediaItemsFetcher fetcher, CharSequence title) {
- MediaDrawerAdapter subAdapter = new MediaDrawerAdapter(mContext, mDrawerController);
- subAdapter.setFetcher(fetcher);
- subAdapter.setTitle(title);
- subAdapter.setFetchCallback(this);
- mDrawerController.pushAdapter(subAdapter);
- }
-
- /**
- * Opens the drawer and displays the current playing queue of items. When the drawer is closed,
- * the view is switched back to the drawer root.
- */
- public void showPlayQueue() {
- mRootAdapter.setFetcherAndInvoke(createMediaQueueItemsFetcher());
- mRootAdapter.setTitle(mMediaPlaybackModel.getQueueTitle());
- mDrawerController.openDrawer();
- mRootAdapter.scrollToCurrent();
- mDrawerController.addDrawerListener(mQueueDrawerListener);
- }
-
- public void cleanup() {
- mDrawerController.removeDrawerListener(mQueueDrawerListener);
- mRootAdapter.cleanup();
- mMediaPlaybackModel.removeListener(mModelListener);
- mMediaPlaybackModel.stop();
- }
-
- /**
- * @return Adapter to display root items of MediaBrowse tree. {@link #showPlayQueue()} can
- * be used to display items from the queue.
- */
- public CarDrawerAdapter getRootAdapter() {
- return mRootAdapter;
- }
-
- /**
- * Creates a {@link MediaBrowserItemsFetcher} that whose root is the given {@code mediaId}.
- */
- private MediaBrowserItemsFetcher createMediaBrowserItemFetcher(String mediaId,
- boolean showQueueItem) {
- return new MediaBrowserItemsFetcher(mContext, mMediaPlaybackModel, this /* listener */,
- mediaId, showQueueItem);
- }
-
- /**
- * Creates a {@link MediaQueueItemsFetcher} that is responsible for fetching items in the user's
- * current play queue.
- */
- private MediaQueueItemsFetcher createMediaQueueItemsFetcher() {
- return new MediaQueueItemsFetcher(mContext, mMediaPlaybackModel, this /* listener */);
- }
-
- /**
- * Creates a {@link MediaItemsFetcher} that will display the top-most level of the drawer.
- */
- private MediaItemsFetcher createRootMediaItemsFetcher() {
- return createMediaBrowserItemFetcher(mMediaPlaybackModel.getMediaBrowser().getRoot(),
- true /* showQueueItem */);
- }
-
- /**
- * A {@link androidx.drawerlayout.widget.DrawerLayout.DrawerListener} specifically to be used when
- * the play queue has been shown in the drawer. When the drawer is closed following this
- * display, this listener will reset the drawer to display the root view.
- */
- private final DrawerLayout.DrawerListener mQueueDrawerListener =
- new DrawerLayout.DrawerListener() {
- @Override
- public void onDrawerClosed(View drawerView) {
- mRootAdapter.setFetcherAndInvoke(createRootMediaItemsFetcher());
- mRootAdapter.setTitle(
- MediaManager.getInstance(mContext).getMediaClientName());
- mDrawerController.removeDrawerListener(this);
- }
-
- @Override
- public void onDrawerSlide(View drawerView, float slideOffset) {}
- @Override
- public void onDrawerOpened(View drawerView) {}
- @Override
- public void onDrawerStateChanged(int newState) {}
- };
-
- private final MediaPlaybackModel.Listener mModelListener =
- new MediaPlaybackModel.AbstractListener() {
- @Override
- public void onMediaAppChanged(@Nullable ComponentName currentName,
- @Nullable ComponentName newName) {
- // Only store MediaManager instance to a local variable when it is short lived.
- MediaManager mediaManager = MediaManager.getInstance(mContext);
- mRootAdapter.cleanup();
- mRootAdapter.setTitle(mediaManager.getMediaClientName());
- }
-
- @Override
- public void onMediaConnected() {
- mRootAdapter.setFetcherAndInvoke(createRootMediaItemsFetcher());
- }
- };
-}
diff --git a/src/com/android/car/media/drawer/MediaItemOnClickListener.java b/src/com/android/car/media/drawer/MediaItemOnClickListener.java
deleted file mode 100644
index f99c7e1..0000000
--- a/src/com/android/car/media/drawer/MediaItemOnClickListener.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.media.drawer;
-
-import android.media.browse.MediaBrowser;
-import android.media.session.MediaSession;
-
-/**
- * Interface for an object that will be notified when an item in the play queue has been clicked.
- */
-interface MediaItemOnClickListener {
- /**
- * Called when an item in the queue has been clicked.
- *
- * @param queueItem The {@link MediaSession.QueueItem} corresponding to the one that has been
- * clicked.
- */
- void onQueueItemClicked(MediaSession.QueueItem queueItem);
-
- /**
- * Called when an item in a list of playable media items has been clicked.
- *
- * @param mediaItem The {@link MediaBrowser.MediaItem} corresponding to the one that been
- * clicked.
- */
- void onMediaItemClicked(MediaBrowser.MediaItem mediaItem);
-}
diff --git a/src/com/android/car/media/drawer/MediaItemsFetcher.java b/src/com/android/car/media/drawer/MediaItemsFetcher.java
deleted file mode 100644
index 2dabd7e..0000000
--- a/src/com/android/car/media/drawer/MediaItemsFetcher.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.media.drawer;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.media.MediaDescription;
-import android.text.TextUtils;
-import android.view.View;
-
-import androidx.car.drawer.CarDrawerAdapter;
-import androidx.car.drawer.DrawerItemViewHolder;
-
-import com.android.car.apps.common.BitmapDownloader;
-import com.android.car.apps.common.BitmapWorkerOptions;
-import com.android.car.apps.common.UriUtils;
-import com.android.car.media.R;
-
-/**
- * Component that handles fetching of items for {@link MediaDrawerAdapter}.
- * <p>
- * It also handles ViewHolder population and item clicks.
- */
-interface MediaItemsFetcher {
- int DONT_SCROLL = -1;
-
- /**
- * Used to inform owning {@link MediaDrawerAdapter} that items have changed.
- */
- interface ItemsUpdatedCallback {
- void onItemsUpdated();
- }
-
- /**
- * Kick-off fetching/monitoring of items.
- *
- * @param callback Callback that is invoked when items are first loaded ar if they change
- * subsequently.
- */
- void start(ItemsUpdatedCallback callback);
-
- /**
- * @return Number of items currently fetched.
- */
- int getItemCount();
-
- /**
- * Used to indicate the kind of layout (small or normal) to use for the views that will display
- * this item in a {@link CarDrawerAdapter}. See {@link CarDrawerAdapter#usesSmallLayout}
- *
- * @param position Adapter position of item
- * @return Whether to use small (true) or normal layout (false).
- */
- boolean usesSmallLayout(int position);
-
- /**
- * Used by owning {@link MediaDrawerAdapter} to populate views.
- *
- * @param holder View-holder to populate.
- * @param position Item position.
- */
- void populateViewHolder(DrawerItemViewHolder holder, int position);
-
- /**
- * Used by owning {@link MediaDrawerAdapter} to handle clicks.
- *
- * @param position Item position.
- */
- void onItemClick(int position);
-
- /**
- * Used when this instance is going to be released. Subclasses should release resources.
- */
- void cleanup();
-
-
- /**
- * Get the position to scroll to if any.
- * @return An integer greater than or equal to 0 if there is a position to scroll to, the
- * constant {@link DONT_SCROLL} otherwise.
- */
- int getScrollPosition();
-
- /**
- * Utility method to determine if description can be displayed in a small layout.
- */
- static boolean usesSmallLayout(MediaDescription description) {
- // Small layout is sufficient if there's no sub-title to display for the item.
- return TextUtils.isEmpty(description.getSubtitle());
- }
-
- /**
- * Utility method to populate {@code holder} with details from {@code description}. It populates
- * title, text and icon at most.
- */
- static void populateViewHolderFrom(DrawerItemViewHolder holder, MediaDescription description) {
- Context context = holder.itemView.getContext();
- holder.getTitle().setText(description.getTitle());
- // If normal layout, populate subtitle.
- if (!usesSmallLayout(description)) {
- holder.getText().setText(description.getSubtitle());
- }
- Bitmap iconBitmap = description.getIconBitmap();
- holder.getIcon().setImageBitmap(iconBitmap); // Ok to set null here for clearing.
- if (iconBitmap == null) {
- if (description.getIconUri() != null) {
- holder.getIcon().setVisibility(View.VISIBLE);
- int bitmapSize =
- context.getResources().getDimensionPixelSize(R.dimen.car_primary_icon_size);
- // We don't want to cache android resources as they are needed to be refreshed after
- // configuration changes.
- int cacheFlag = UriUtils.isAndroidResourceUri(description.getIconUri())
- ? (BitmapWorkerOptions.CACHE_FLAG_DISK_DISABLED
- | BitmapWorkerOptions.CACHE_FLAG_MEM_DISABLED)
- : 0;
- BitmapWorkerOptions options = new BitmapWorkerOptions.Builder(context)
- .resource(description.getIconUri())
- .height(bitmapSize)
- .width(bitmapSize)
- .cacheFlag(cacheFlag)
- .build();
- BitmapDownloader.getInstance(context).loadBitmap(options, holder.getIcon());
- } else {
- holder.getIcon().setVisibility(View.GONE);
- }
- }
- }
-}
diff --git a/src/com/android/car/media/drawer/MediaQueueItemsFetcher.java b/src/com/android/car/media/drawer/MediaQueueItemsFetcher.java
deleted file mode 100644
index 301b4f8..0000000
--- a/src/com/android/car/media/drawer/MediaQueueItemsFetcher.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.media.drawer;
-
-import android.content.Context;
-import android.graphics.PorterDuff;
-import android.graphics.drawable.Drawable;
-import android.media.session.MediaController;
-import android.media.session.MediaSession;
-import android.media.session.PlaybackState;
-import android.os.Handler;
-
-import androidx.annotation.Nullable;
-import androidx.car.drawer.DrawerItemViewHolder;
-
-import com.android.car.media.MediaPlaybackModel;
-import com.android.car.media.R;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * {@link MediaItemsFetcher} implementation that fetches items from the {@link MediaController}'s
- * currently playing queue.
- */
-class MediaQueueItemsFetcher implements MediaItemsFetcher {
- private final Handler mHandler = new Handler();
- private final Context mContext;
- private final MediaItemOnClickListener mClickListener;
- private MediaPlaybackModel mMediaPlaybackModel;
- private ItemsUpdatedCallback mCallback;
- private List<MediaSession.QueueItem> mItems = new ArrayList<>();
-
- MediaQueueItemsFetcher(Context context, MediaPlaybackModel model,
- MediaItemOnClickListener listener) {
- mContext = context;
- mMediaPlaybackModel = model;
- mClickListener = listener;
- }
-
- @Override
- public void start(ItemsUpdatedCallback callback) {
- mCallback = callback;
- if (mMediaPlaybackModel != null) {
- mMediaPlaybackModel.addListener(mListener);
- updateItemsFrom(mMediaPlaybackModel.getQueue());
- }
- // Inform client of current items. Invoke async to avoid re-entrancy issues.
- mHandler.post(mCallback::onItemsUpdated);
- }
-
- @Override
- public int getItemCount() {
- return mItems.size();
- }
-
- @Override
- public boolean usesSmallLayout(int position) {
- return MediaItemsFetcher.usesSmallLayout(mItems.get(position).getDescription());
- }
-
- @Override
- public void populateViewHolder(DrawerItemViewHolder holder, int position) {
- MediaSession.QueueItem item = mItems.get(position);
- MediaItemsFetcher.populateViewHolderFrom(holder, item.getDescription());
-
- if (holder.getEndIcon() == null) {
- return;
- }
-
- if (item.getQueueId() == getActiveQueueItemId()) {
- int primaryColor = mMediaPlaybackModel.getPrimaryColor();
- Drawable drawable =
- mContext.getDrawable(R.drawable.ic_music_active);
- drawable.setColorFilter(primaryColor, PorterDuff.Mode.SRC_IN);
- holder.getEndIcon().setImageDrawable(drawable);
- } else {
- holder.getEndIcon().setImageBitmap(null);
- }
- }
-
- @Override
- public void onItemClick(int position) {
- if (mClickListener != null) {
- mClickListener.onQueueItemClicked(mItems.get(position));
- }
- }
-
- @Override
- public void cleanup() {
- mMediaPlaybackModel.removeListener(mListener);
- }
-
- @Override
- public int getScrollPosition() {
- long activeId = getActiveQueueItemId();
- // A linear scan isn't really the best thing to do for large lists but we suspect that
- // the queue isn't going to be very long anyway so we can just do the trivial thing. If
- // it starts becoming a problem, we can build an index over the ids.
- for (int position = 0; position < mItems.size(); position++) {
- MediaSession.QueueItem item = mItems.get(position);
- if (item.getQueueId() == activeId) {
- return position;
- }
- }
- return MediaItemsFetcher.DONT_SCROLL;
- }
-
- private void updateItemsFrom(List<MediaSession.QueueItem> queue) {
- mItems.clear();
- mItems.addAll(queue);
- }
-
- private long getActiveQueueItemId() {
- if (mMediaPlaybackModel != null) {
- PlaybackState playbackState = mMediaPlaybackModel.getPlaybackState();
- if (playbackState != null) {
- return playbackState.getActiveQueueItemId();
- }
- }
- return MediaSession.QueueItem.UNKNOWN_ID;
- }
-
- private final MediaPlaybackModel.Listener mListener =
- new MediaPlaybackModel.AbstractListener() {
- @Override
- public void onQueueChanged(List<MediaSession.QueueItem> queue) {
- updateItemsFrom(queue);
- mCallback.onItemsUpdated();
- }
-
- @Override
- public void onPlaybackStateChanged(@Nullable PlaybackState state) {
- // Since active playing item may have changed, force re-draw of queue items.
- mCallback.onItemsUpdated();
- }
-
- @Override
- public void onSessionDestroyed(CharSequence destroyedMediaClientName) {
- onQueueChanged(Collections.emptyList());
- }
- };
-}
diff --git a/src/com/android/car/media/widgets/AppBarView.java b/src/com/android/car/media/widgets/AppBarView.java
index dfef68a..6acb1c9 100644
--- a/src/com/android/car/media/widgets/AppBarView.java
+++ b/src/com/android/car/media/widgets/AppBarView.java
@@ -2,62 +2,60 @@ package com.android.car.media.widgets;
import android.annotation.Nullable;
import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
-import android.transition.Fade;
-import android.transition.Transition;
-import android.transition.TransitionManager;
import android.util.AttributeSet;
import android.util.Log;
-import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.RelativeLayout;
import android.widget.TextView;
+import androidx.constraintlayout.widget.ConstraintLayout;
+
+import com.android.car.apps.common.UxrButton;
+import com.android.car.apps.common.widget.CarTabLayout;
import com.android.car.media.R;
+import com.android.car.media.common.MediaAppSelectorWidget;
import com.android.car.media.common.MediaItemMetadata;
-import com.google.android.material.tabs.TabLayout;
-
import java.util.List;
import java.util.Objects;
/**
- * Media template application bar. A detailed explanation of all possible states of this
- * application bar can be seen at {@link AppBarView.State}.
+ * Media template application bar. The callers should set properties via the public methods (e.g.,
+ * {@link setItems()}, {@link setTitle()}, {@link setHasSettings()}), and set the visibility of the
+ * views via {@link setState()}. A detailed explanation of all possible states of this application
+ * bar can be seen at {@link AppBarView.State}.
*/
-public class AppBarView extends RelativeLayout {
+public class AppBarView extends ConstraintLayout {
private static final String TAG = "AppBarView";
- /** Default number of tabs to show on this app bar */
- private static int DEFAULT_MAX_TABS = 4;
- private LinearLayout mTabsContainer;
- private ImageView mAppIcon;
- private ImageView mAppSwitchIcon;
+ private CarTabLayout<MediaItemTab> mTabsContainer;
private ImageView mNavIcon;
private ViewGroup mNavIconContainer;
private TextView mTitle;
- private ViewGroup mAppSwitchContainer;
+ /** Visible if mHasSettings && mShowSettings. */
+ private UxrButton mSettingsButton;
+ private boolean mHasSettings;
+ private boolean mShowSettings;
+ private View mSearchButton;
+ private SearchBar mSearchBar;
+ private MediaAppSelectorWidget mAppSelector;
private Context mContext;
private int mMaxTabs;
- private Drawable mArrowDropDown;
- private Drawable mArrowDropUp;
private Drawable mArrowBack;
private Drawable mCollapse;
private State mState = State.BROWSING;
private AppBarListener mListener;
private int mFadeDuration;
- private float mSelectedTabAlpha;
- private float mUnselectedTabAlpha;
- private MediaItemMetadata mSelectedItem;
private String mMediaAppTitle;
- private Drawable mDefaultIcon;
- private boolean mContentForwardEnabled;
+ private boolean mSearchSupported;
+ private int mMaxRows;
+
+ public interface AppBarProvider {
+ AppBarView getAppBar();
+ }
/**
* Application bar listener
@@ -74,14 +72,19 @@ public class AppBarView extends RelativeLayout {
void onBack();
/**
- * Invoked when the user clicks on the collapse button
+ * Invoked when the user clicks on the settings button.
+ */
+ void onSettingsSelection();
+
+ /**
+ * Invoked when the user submits a search query.
*/
- void onCollapse();
+ void onSearch(String query);
/**
- * Invoked when the user clicks on the app selection switch
+ * Invoked when the user clicks on the search button
*/
- void onAppSelection();
+ void onSearchSelection();
}
/**
@@ -99,16 +102,15 @@ public class AppBarView extends RelativeLayout {
*/
STACKED,
/**
- * Indicates that we have expanded a view that can be collapsed. We show the
- * title of the application and a collapse icon
+ * Indicates that the user is currently entering a search query. We show the search bar and
+ * a collapse icon
*/
- PLAYING,
+ SEARCHING,
/**
- * Used to indicate that the user is inside the app selector. In this case we disable
- * navigation, we show the title of the application and we show the app switch icon
- * point up
+ * Used whenever the app bar should not display any information such as when MediaCenter
+ * is in an error state
*/
- APP_SELECTION
+ EMPTY
}
public AppBarView(Context context) {
@@ -120,69 +122,85 @@ public class AppBarView extends RelativeLayout {
}
public AppBarView(Context context, AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
+ super(context, attrs, defStyleAttr);
+ init(context, attrs, defStyleAttr);
}
- public AppBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- init(context, attrs, defStyleAttr, defStyleRes);
- }
-
- private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- TypedArray ta = context.obtainStyledAttributes(
- attrs, R.styleable.AppBarView, defStyleAttr, defStyleRes);
- mMaxTabs = ta.getInteger(R.styleable.AppBarView_max_tabs, DEFAULT_MAX_TABS);
- ta.recycle();
+ private void init(Context context, AttributeSet attrs, int defStyleAttr) {
+ mMaxTabs = context.getResources().getInteger(R.integer.max_tabs);
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.appbar_view, this, true);
mContext = context;
+ mMaxRows = mContext.getResources().getInteger(R.integer.num_app_bar_view_rows);
+
mTabsContainer = findViewById(R.id.tabs);
+ mTabsContainer.addOnCarTabSelectedListener(
+ new CarTabLayout.SimpleOnCarTabSelectedListener<MediaItemTab>() {
+ @Override
+ public void onCarTabSelected(MediaItemTab mediaItemTab) {
+ if (mListener != null) {
+ mListener.onTabSelected(mediaItemTab.getItem());
+ }
+ }
+ });
mNavIcon = findViewById(R.id.nav_icon);
mNavIconContainer = findViewById(R.id.nav_icon_container);
mNavIconContainer.setOnClickListener(view -> onNavIconClicked());
- mAppIcon = findViewById(R.id.app_icon);
- mAppSwitchIcon = findViewById(R.id.app_switch_icon);
- mAppSwitchContainer = findViewById(R.id.app_switch_container);
- mAppSwitchContainer.setOnClickListener(view -> onAppSwitchClicked());
+ mAppSelector = findViewById(R.id.app_switch_container);
+ mSettingsButton = findViewById(R.id.settings);
+ mSettingsButton.setOnClickListener(view -> onSettingsClicked());
+ mSearchButton = findViewById(R.id.search);
+ mSearchButton.setOnClickListener(view -> onSearchClicked());
+ mSearchBar = findViewById(R.id.search_bar_container);
+
mTitle = findViewById(R.id.title);
- mArrowDropDown = getResources().getDrawable(R.drawable.ic_arrow_drop_down, null);
- mArrowDropUp = getResources().getDrawable(R.drawable.ic_arrow_drop_up, null);
mArrowBack = getResources().getDrawable(R.drawable.ic_arrow_back, null);
mCollapse = getResources().getDrawable(R.drawable.ic_expand_more, null);
mFadeDuration = getResources().getInteger(R.integer.app_selector_fade_duration);
- TypedValue outValue = new TypedValue();
- getResources().getValue(R.dimen.browse_tab_alpha_selected, outValue, true);
- mSelectedTabAlpha = outValue.getFloat();
- getResources().getValue(R.dimen.browse_tab_alpha_unselected, outValue, true);
- mUnselectedTabAlpha = outValue.getFloat();
mMediaAppTitle = getResources().getString(R.string.media_app_title);
- mDefaultIcon = getResources().getDrawable(R.drawable.ic_music);
setState(State.BROWSING);
}
+ public void openAppSelector() {
+ mAppSelector.open();
+ }
+
+ public void closeAppSelector() {
+ mAppSelector.close();
+ }
+
private void onNavIconClicked() {
if (mListener == null) {
return;
}
switch (mState) {
+ case BROWSING:
case STACKED:
mListener.onBack();
break;
- case PLAYING:
- mListener.onCollapse();
+ case SEARCHING:
+ mSearchBar.showSearchBar(false);
+ mListener.onBack();
break;
}
}
- private void onAppSwitchClicked() {
+ private void onSettingsClicked() {
if (mListener == null) {
return;
}
- mListener.onAppSelection();
+ mListener.onSettingsSelection();
+ }
+
+ private void onSearchClicked() {
+ if (mListener == null) {
+ return;
+ }
+ mListener.onSearchSelection();
}
/**
@@ -199,27 +217,13 @@ public class AppBarView extends RelativeLayout {
* @param items list of tabs to show, or null if no tabs should be shown.
*/
public void setItems(@Nullable List<MediaItemMetadata> items) {
- mTabsContainer.removeAllViews();
+ mTabsContainer.clearAllCarTabs();
- if (items != null) {
+ if (items != null && !items.isEmpty()) {
int count = 0;
- int padding = mContext.getResources().getDimensionPixelSize(R.dimen.car_padding_4);
- int tabWidth = mContext.getResources().getDimensionPixelSize(R.dimen.browse_tab_width) +
- 2 * padding;
- LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
- tabWidth, ViewGroup.LayoutParams.MATCH_PARENT);
for (MediaItemMetadata item : items) {
- MediaItemTabView tab = new MediaItemTabView(mContext, item);
- mTabsContainer.addView(tab);
- tab.setLayoutParams(layoutParams);
- tab.setOnClickListener(view -> {
- if (mListener != null) {
- mListener.onTabSelected(item);
- }
- });
- tab.setPadding(padding, 0, padding, 0);
- tab.requestLayout();
- tab.setTag(item);
+ MediaItemTab tab = new MediaItemTab(mContext, item);
+ mTabsContainer.addCarTab(tab);
count++;
if (count >= mMaxTabs) {
@@ -233,99 +237,117 @@ public class AppBarView extends RelativeLayout {
}
/**
- * Updates the title to display when the bar is not showing tabs.
+ * Updates the title to display when the bar is not showing tabs. If the provided title is null,
+ * will default to displaying the app name.
*/
public void setTitle(CharSequence title) {
mTitle.setText(title != null ? title : mMediaAppTitle);
}
/**
- * Whether content forward browsing is enabled or not
+ * Sets the name of the currently displayed media app. This is used as the default title for
+ * playback and the root browse menu. If provided title is null, will use default media center
+ * title.
*/
- public void setContentForwardEnabled(boolean enabled) {
- mContentForwardEnabled = enabled;
+ public void setMediaAppTitle(CharSequence appTitle) {
+ mMediaAppTitle = appTitle == null ? getResources().getString(R.string.media_app_title)
+ : appTitle.toString();
+ }
+
+ /** Sets whether the source has settings (not all screens show it). */
+ public void setHasSettings(boolean hasSettings) {
+ mHasSettings = hasSettings;
+ updateSettingsVisibility();
+ }
+
+ private void showSettings(boolean showSettings) {
+ mShowSettings = showSettings;
+ updateSettingsVisibility();
+ }
+
+ private void updateSettingsVisibility() {
+ mSettingsButton.setVisibility(mHasSettings && mShowSettings ? VISIBLE : GONE);
}
/**
- * Updates the application icon to show next to the application switcher.
+ * Updates the currently active item
*/
- public void setAppIcon(Bitmap icon) {
- if (icon != null) {
- mAppIcon.setImageBitmap(icon);
- } else {
- mAppIcon.setImageDrawable(mDefaultIcon);
+ public void setActiveItem(MediaItemMetadata item) {
+ for (int i = 0; i < mTabsContainer.getCarTabCount(); i++) {
+ MediaItemTab mediaItemTab = mTabsContainer.get(i);
+ boolean match = item != null && Objects.equals(
+ item.getId(),
+ mediaItemTab.getItem().getId());
+ if (match) {
+ mTabsContainer.selectCarTab(mediaItemTab);
+ return;
+ }
}
}
/**
- * Indicates whether or not the application switcher should be enabled.
+ * Sets whether the search box should be shown
*/
- public void setAppSelection(boolean enabled) {
- mAppSwitchIcon.setVisibility(enabled ? View.VISIBLE : View.GONE);
+ public void setSearchSupported(boolean supported) {
+ mSearchSupported = supported;
+ mSearchButton.setVisibility(mSearchSupported ? View.VISIBLE : View.GONE);
}
/**
- * Updates the currently active item
+ * Sets whether to show tabs or not. The caller should make sure tabs has at least 1 item before
+ * showing tabs.
*/
- public void setActiveItem(MediaItemMetadata item) {
- mSelectedItem = item;
- // TODO(b/79264184): Updating tabs alpha is causing them to disappear randomly. We are
- // de-activating this feature for not.
- // updateTabs();
- }
-
- private void updateTabs() {
- for (int i = 0; i < mTabsContainer.getChildCount(); i++) {
- View child = mTabsContainer.getChildAt(i);
- if (child instanceof MediaItemTabView) {
- MediaItemTabView tabView = (MediaItemTabView) child;
- boolean match = mSelectedItem != null && Objects.equals(
- mSelectedItem.getId(),
- ((MediaItemMetadata) tabView.getTag()).getId());
- tabView.setAlpha(match ? mSelectedTabAlpha : mUnselectedTabAlpha);
- }
- }
+ private void setShowTabs(boolean visible) {
+ // Refresh state to adjust for new tab visibility
+ mTabsContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
}
/**
* Updates the state of the bar.
*/
public void setState(State state) {
- boolean hasItems = mTabsContainer.getChildCount() > 0;
mState = state;
-
- Transition transition = new Fade().setDuration(mFadeDuration);
- TransitionManager.beginDelayedTransition(this, transition);
- Log.d(TAG, "Updating state: " + state + " (has items: " + hasItems + ")");
+ final boolean hasTabs = mTabsContainer.getCarTabCount() > 0;
+ final boolean showTitle = !hasTabs || mMaxRows == 2;
+ Log.d(TAG, "Updating state: " + state + " (has tabs: " + hasTabs + ")");
switch (state) {
+ case EMPTY:
+ mNavIconContainer.setVisibility(View.GONE);
+ setShowTabs(false);
+ mTitle.setVisibility(View.GONE);
+ mSearchBar.showSearchBar(false);
+ showSettings(true);
+ mAppSelector.setVisibility(View.VISIBLE);
+ break;
case BROWSING:
+ mNavIcon.setImageDrawable(mArrowBack);
mNavIconContainer.setVisibility(View.GONE);
- mTabsContainer.setVisibility(hasItems ? View.VISIBLE : View.GONE);
- mTitle.setVisibility(hasItems ? View.GONE : View.VISIBLE);
- mAppSwitchIcon.setImageDrawable(mArrowDropDown);
+ setShowTabs(hasTabs);
+ mTitle.setVisibility(showTitle ? View.VISIBLE : View.GONE);
+ mSearchBar.showSearchBar(false);
+ mSearchButton.setVisibility(mSearchSupported ? View.VISIBLE : View.GONE);
+ showSettings(true);
+ mAppSelector.setVisibility(View.VISIBLE);
break;
case STACKED:
mNavIcon.setImageDrawable(mArrowBack);
mNavIconContainer.setVisibility(View.VISIBLE);
- mTabsContainer.setVisibility(View.GONE);
+ setShowTabs(false);
mTitle.setVisibility(View.VISIBLE);
- mAppSwitchIcon.setImageDrawable(mArrowDropDown);
- break;
- case PLAYING:
- mNavIcon.setImageDrawable(mCollapse);
- mNavIconContainer.setVisibility(hasItems || !mContentForwardEnabled ? View.GONE
- : View.VISIBLE);
- mTabsContainer.setVisibility(hasItems && mContentForwardEnabled ? View.VISIBLE
- : View.GONE);
- mTitle.setVisibility(hasItems || !mContentForwardEnabled ? View.GONE
- : View.VISIBLE);
- mAppSwitchIcon.setImageDrawable(mArrowDropDown);
+ mSearchBar.showSearchBar(false);
+ mSearchButton.setVisibility(mSearchSupported ? View.VISIBLE : View.GONE);
+ showSettings(true);
+ mAppSelector.setVisibility(View.VISIBLE);
break;
- case APP_SELECTION:
- mNavIconContainer.setVisibility(View.GONE);
- mTabsContainer.setVisibility(View.GONE);
- mTitle.setVisibility(mContentForwardEnabled ? View.VISIBLE : View.GONE);
- mAppSwitchIcon.setImageDrawable(mArrowDropUp);
+ case SEARCHING:
+ mNavIcon.setImageDrawable(mArrowBack);
+ mNavIconContainer.setVisibility(View.VISIBLE);
+ setShowTabs(false);
+ mTitle.setVisibility(View.GONE);
+ mSearchBar.showSearchBar(true);
+ mSearchButton.setVisibility(View.GONE);
+ showSettings(false);
+ mAppSelector.setVisibility(View.GONE);
break;
}
}
diff --git a/src/com/android/car/media/widgets/MediaItemTab.java b/src/com/android/car/media/widgets/MediaItemTab.java
new file mode 100644
index 0000000..519f188
--- /dev/null
+++ b/src/com/android/car/media/widgets/MediaItemTab.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.media.widgets;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.widget.ImageView;
+
+import com.android.car.apps.common.widget.CarTabLayout;
+import com.android.car.media.common.MediaItemMetadata;
+
+/**
+ * An entity representing a media item to be included in the tab bar at the top of the UI.
+ */
+public class MediaItemTab extends CarTabLayout.CarTab {
+ private final MediaItemMetadata mItem;
+
+ /**
+ * Creates a new tab for the given media item.
+ */
+ public MediaItemTab(@NonNull Context context, @NonNull MediaItemMetadata item) {
+ super(null, item.getTitle());
+ mItem = item;
+ }
+
+ @Override
+ protected void bindIcon(ImageView imageView) {
+ MediaItemMetadata.updateImageView(imageView.getContext(), mItem, imageView, 0, false);
+ }
+
+ /**
+ * Returns the item represented by this view
+ */
+ @NonNull
+ public MediaItemMetadata getItem() {
+ return mItem;
+ }
+}
diff --git a/src/com/android/car/media/widgets/MediaItemTabView.java b/src/com/android/car/media/widgets/MediaItemTabView.java
deleted file mode 100644
index 581a373..0000000
--- a/src/com/android/car/media/widgets/MediaItemTabView.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.media.widgets;
-
-import android.annotation.NonNull;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.android.car.media.R;
-import com.android.car.media.common.MediaItemMetadata;
-
-/**
- * A view representing a media item to be included in the tab bar at the top of the UI.
- */
-public class MediaItemTabView extends LinearLayout {
- private final TextView mTitleView;
- private final ImageView mImageView;
- private final MediaItemMetadata mItem;
-
- /**
- * Creates a new tab for the given media item.
- */
- public MediaItemTabView(@NonNull Context context, @NonNull MediaItemMetadata item) {
- super(context);
- LayoutInflater inflater = (LayoutInflater) context
- .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- inflater.inflate(R.layout.tab_view, this, true);
- setOrientation(LinearLayout.VERTICAL);
- setFocusable(true);
- setGravity(Gravity.CENTER);
- setBackground(context.getDrawable(R.drawable.app_item_background));
-
- int[] attrs = new int[]{android.R.attr.selectableItemBackground};
- TypedArray typedArray = context.obtainStyledAttributes(attrs);
- int backgroundResource = typedArray.getResourceId(0, 0);
- setBackgroundResource(backgroundResource);
-
- mItem = item;
- mImageView = findViewById(R.id.icon);
- MediaItemMetadata.updateImageView(context, item, mImageView, 0);
- mTitleView = findViewById(R.id.title);
- mTitleView.setText(item.getTitle());
- }
-
- /**
- * Returns the item represented by this view
- */
- @NonNull
- public MediaItemMetadata getItem() {
- return mItem;
- }
-}
diff --git a/src/com/android/car/media/widgets/MetadataView.java b/src/com/android/car/media/widgets/MetadataView.java
deleted file mode 100644
index 6d9e72a..0000000
--- a/src/com/android/car/media/widgets/MetadataView.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package com.android.car.media.widgets;
-
-import android.annotation.Nullable;
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.widget.RelativeLayout;
-import android.widget.SeekBar;
-import android.widget.TextView;
-
-import com.android.car.media.MetadataController;
-import com.android.car.media.R;
-import com.android.car.media.common.PlaybackModel;
-
-/**
- * A view that can be used to display the metadata and playback progress of a media item.
- * This view can be styled using the "MetadataView" styleable attributes.
- */
-public class MetadataView extends RelativeLayout {
- private final MetadataController mMetadataController;
-
- public MetadataView(Context context) {
- this(context, null);
- }
-
- public MetadataView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public MetadataView(Context context, AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
- }
-
- public MetadataView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- LayoutInflater.from(context).inflate(R.layout.metadata_compact, this, true);
- TextView title = findViewById(R.id.title);
- TextView subtitle = findViewById(R.id.subtitle);
- SeekBar seekBar = findViewById(R.id.seek_bar);
- mMetadataController = new MetadataController(title, subtitle, null, seekBar, null);
- }
-
- /**
- * Registers the {@link PlaybackModel} this widget will use to follow playback state.
- * Consumers of this class must unregister the {@link PlaybackModel} by calling this method with
- * null.
- *
- * @param model {@link PlaybackModel} to subscribe, or null to unsubscribe.
- */
- public void setModel(@Nullable PlaybackModel model) {
- mMetadataController.setModel(model);
- }
-
-}
diff --git a/src/com/android/car/media/widgets/SearchBar.java b/src/com/android/car/media/widgets/SearchBar.java
new file mode 100644
index 0000000..0a5e09a
--- /dev/null
+++ b/src/com/android/car/media/widgets/SearchBar.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.media.widgets;
+
+import android.content.Context;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.car.media.R;
+import com.android.car.media.common.MediaAppSelectorWidget;
+
+/**
+ * This widget represents a search bar that shows the media source's icon as part of the search bar
+ */
+public class SearchBar extends LinearLayout {
+
+ private final MediaAppSelectorWidget mAppIcon;
+ private final EditText mSearchText;
+ private final ImageView mCloseIcon;
+
+ private AppBarView.AppBarListener mListener;
+
+ public SearchBar(Context context) {
+ this(context, null);
+ }
+
+ public SearchBar(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public SearchBar(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public SearchBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ LayoutInflater inflater = LayoutInflater.from(context);
+ inflater.inflate(R.layout.search_bar, this, true);
+
+ mSearchText = findViewById(R.id.search_bar);
+ mCloseIcon = findViewById(R.id.search_close);
+ mCloseIcon.setOnClickListener(view -> mSearchText.getText().clear());
+ mAppIcon = findViewById(R.id.app_icon_container);
+
+ mSearchText.setOnFocusChangeListener(
+ (view, hasFocus) -> {
+ if (hasFocus) {
+ mSearchText.setCursorVisible(true);
+ ((InputMethodManager)
+ context.getSystemService(Context.INPUT_METHOD_SERVICE))
+ .showSoftInput(view, 0);
+ } else {
+ mSearchText.setCursorVisible(false);
+ ((InputMethodManager)
+ context.getSystemService(Context.INPUT_METHOD_SERVICE))
+ .hideSoftInputFromWindow(view.getWindowToken(), 0);
+ }
+ });
+ mSearchText.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ onSearch(editable.toString());
+ }
+ });
+ mSearchText.setOnEditorActionListener((v, actionId, event) -> {
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ mSearchText.setCursorVisible(false);
+ }
+ return false;
+ });
+ }
+
+ /** Calling this is required so the widget can show the icon of the primary media source. */
+ public void setFragmentActivity(FragmentActivity activity) {
+
+ mAppIcon.setFragmentActivity(activity);
+ }
+
+ public void setAppBarListener(AppBarView.AppBarListener listener) {
+ mListener = listener;
+ }
+
+ public void showSearchBar(boolean visible) {
+ if (visible) {
+ setVisibility(VISIBLE);
+ mSearchText.requestFocus();
+ } else{
+ setVisibility(GONE);
+ mSearchText.getText().clear();
+ }
+ }
+
+ private void onSearch(String query) {
+ if (mListener == null || TextUtils.isEmpty(query)) {
+ return;
+ }
+ mListener.onSearch(query);
+ }
+}
diff --git a/src/com/android/car/media/widgets/ViewUtils.java b/src/com/android/car/media/widgets/ViewUtils.java
deleted file mode 100644
index 6f7ad9a..0000000
--- a/src/com/android/car/media/widgets/ViewUtils.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package com.android.car.media.widgets;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.annotation.NonNull;
-import android.view.View;
-
-/**
- * Utility methods to operate over views.
- */
-public class ViewUtils {
- /**
- * Hides a view using a fade-out animation
- *
- * @param view {@link View} to be hidden
- * @param duration animation duration in milliseconds.
- */
- public static void hideViewAnimated(@NonNull View view, int duration) {
- if (view.getVisibility() == View.GONE) {
- return;
- }
- if (!view.isLaidOut()) {
- // If the view hasn't been displayed yet, just adjust visibility without animation
- view.setVisibility(View.GONE);
- return;
- }
- view.animate()
- .alpha(0f)
- .setDuration(duration)
- .setListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- view.setVisibility(View.GONE);
- }
- });
- }
-
- /**
- * Shows a view using a fade-in animation
- *
- * @param view {@link View} to be shown
- * @param duration animation duration in milliseconds.
- */
- public static void showViewAnimated(@NonNull View view, int duration) {
- if (view.getVisibility() == View.VISIBLE) {
- return;
- }
- if (!view.isLaidOut()) {
- // If the view hasn't been displayed yet, just adjust visibility without animation
- view.setVisibility(View.VISIBLE);
- return;
- }
- view.animate()
- .alpha(1f)
- .setDuration(duration)
- .setListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animation) {
- view.setAlpha(0f);
- view.setVisibility(View.VISIBLE);
- }
- });
- }
-}