Snap for 9606762 from 5354d747cc0f6e7ec38dfbe53cb874668ac76e87 to car-apps-aosp-release
Change-Id: Ib8aec84f59ea8d2169c81a830aa078d52fe49b43
diff --git a/res/drawable/media_item_divider.xml b/res/drawable/media_item_divider.xml
new file mode 100644
index 0000000..2cd0367
--- /dev/null
+++ b/res/drawable/media_item_divider.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/listview_background_shape">
+ <stroke android:width="0dp" android:color="@color/car_ui_list_item_divider" />
+ <solid android:color="@color/car_ui_list_item_divider" />
+</shape>
diff --git a/res/layout/browse_custom_action.xml b/res/layout/browse_custom_action.xml
new file mode 100644
index 0000000..3753b40
--- /dev/null
+++ b/res/layout/browse_custom_action.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT 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="wrap_content"
+ android:layout_height="match_parent">
+
+ <ImageView
+ android:id="@+id/browse_item_custom_action_divider"
+ android:layout_width="1dp"
+ android:layout_height="@dimen/media_browse_list_item_icons_size"
+ android:src="@drawable/media_item_divider"
+ android:layout_marginStart="@dimen/media_browse_list_item_icon_margin"
+ android:layout_marginEnd="@dimen/media_browse_list_item_icon_margin"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/browse_item_custom_action"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <ImageView
+ android:id="@+id/browse_item_custom_action"
+ android:layout_width="@dimen/media_browse_list_item_icons_size"
+ android:layout_height="@dimen/media_browse_list_item_icons_size"
+ android:scaleType="fitCenter"
+ android:layout_marginStart="@dimen/media_browse_list_item_icon_margin"
+ android:layout_marginEnd="@dimen/media_browse_list_item_icon_margin"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/browse_node.xml b/res/layout/browse_node.xml
index 088a4fe..36ab251 100644
--- a/res/layout/browse_node.xml
+++ b/res/layout/browse_node.xml
@@ -29,8 +29,17 @@
android:layout_marginTop="@dimen/car_ui_toolbar_first_row_height"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- />
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <com.android.car.media.browse.actionbar.BrowseActionsHeader
+ android:id="@+id/toolbar_container"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/car_ui_toolbar_first_row_height"
+ android:layout_marginEnd="@dimen/media_browse_header_action_margin"
+ android:elevation="2dp"
+ app:layout_constraintTop_toBottomOf="@+id/ui_content_top_guideline2"
+ app:layout_constraintEnd_toEndOf="parent"
+ android:visibility="gone" />
<ImageView
android:id="@+id/error_icon"
@@ -64,8 +73,7 @@
android:layout_width="match_parent"
android:layout_height="1dp"
android:orientation="horizontal"
- app:layout_constraintGuide_percent="0.25"
- />
+ app:layout_constraintGuide_percent="0.25" />
<com.android.car.ui.FocusArea
android:id="@+id/focus_area"
@@ -86,5 +94,4 @@
app:layoutStyle="grid"
app:numOfColumns="@integer/num_browse_columns"/>
</com.android.car.ui.FocusArea>
-
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/fragment_playback.xml b/res/layout/fragment_playback.xml
index 3854b49..37ff21f 100644
--- a/res/layout/fragment_playback.xml
+++ b/res/layout/fragment_playback.xml
@@ -96,24 +96,14 @@
android:layout_height="@dimen/fragment_playback_queue_overlap_bottom"
app:layout_constraintTop_toTopOf="@+id/control_bar_first_row_guideline"/>
- <com.android.car.ui.FocusArea
- android:id="@+id/queue_container"
+ <FrameLayout
+ android:id="@+id/queue_fragment_container"
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">
- <com.android.car.ui.recyclerview.CarUiRecyclerView
- android:id="@+id/queue_list"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:visibility="gone"
- android:fadeScrollbars="true"
- android:scrollbars="vertical"
- android:requiresFadingEdge="vertical"
- android:fadingEdgeLength="@dimen/queue_fading_edge_length"/>
- <!-- NOTE: we must specify :scrollbars before :requiresFadingEdge to avoid a crash on R
- (see b/253505704). -->
- </com.android.car.ui.FocusArea>
+ app:layout_constraintBottom_toBottomOf="@+id/queue_list_bottom_constraint"
+ />
<include
layout="@layout/scrim_overlay"
diff --git a/res/layout/fragment_playback_queue.xml b/res/layout/fragment_playback_queue.xml
new file mode 100644
index 0000000..4f4e00c
--- /dev/null
+++ b/res/layout/fragment_playback_queue.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2022, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.android.car.ui.FocusArea
+ android:id="@+id/queue_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <com.android.car.ui.recyclerview.CarUiRecyclerView
+ android:id="@+id/queue_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fadeScrollbars="true"
+ android:scrollbars="vertical"
+ android:requiresFadingEdge="vertical"
+ android:fadingEdgeLength="@dimen/queue_fading_edge_length"/>
+ <!-- NOTE: we must specify :scrollbars before :requiresFadingEdge to avoid a crash on R
+ (see b/253505704). -->
+ </com.android.car.ui.FocusArea>
+
+</FrameLayout>
diff --git a/res/layout/media_browse_grid_icons_item.xml b/res/layout/media_browse_grid_icons_item.xml
index 79b39d0..079215e 100644
--- a/res/layout/media_browse_grid_icons_item.xml
+++ b/res/layout/media_browse_grid_icons_item.xml
@@ -122,7 +122,7 @@
android:layout_marginTop="@dimen/media_browse_progress_bar_top_grid_margin"
android:layout_marginBottom="@dimen/media_browse_progress_bar_bottom_grid_margin"
android:layout_marginEnd="@dimen/media_browse_progress_bar_bottom_grid_margin"
- app:layout_goneMarginEnd="@dimen/media_browse_progress_bar_gone_grid_margin"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintTop_toBottomOf="@+id/subtitle"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
@@ -134,7 +134,7 @@
android:src="@drawable/browser_progress_new_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_margin="@dimen/media_browse_progress_indicator_margin"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintTop_toBottomOf="@+id/subtitle"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
diff --git a/res/layout/media_browse_grid_item.xml b/res/layout/media_browse_grid_item.xml
index abee123..79c8850 100644
--- a/res/layout/media_browse_grid_item.xml
+++ b/res/layout/media_browse_grid_item.xml
@@ -121,7 +121,7 @@
android:layout_marginTop="@dimen/media_browse_progress_bar_top_grid_margin"
android:layout_marginBottom="@dimen/media_browse_progress_bar_bottom_grid_margin"
android:layout_marginEnd="@dimen/media_browse_progress_bar_bottom_grid_margin"
- app:layout_goneMarginEnd="@dimen/media_browse_progress_bar_gone_grid_margin"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintTop_toBottomOf="@+id/subtitle"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
@@ -133,8 +133,8 @@
android:src="@drawable/browser_progress_new_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_margin="@dimen/media_browse_progress_indicator_margin"
android:layout_gravity="center_vertical"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone"/>
diff --git a/res/layout/media_browse_header_item.xml b/res/layout/media_browse_header_item.xml
index 1fa6d47..166b075 100644
--- a/res/layout/media_browse_header_item.xml
+++ b/res/layout/media_browse_header_item.xml
@@ -14,17 +14,30 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<FrameLayout
+<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/item_container"
android:layout_width="match_parent"
android:layout_height="@dimen/media_browse_header_item_height"
- android:layout_marginHorizontal="@dimen/media_browse_header_item_margin_x">
+ android:layout_marginStart="@dimen/media_browse_header_action_margin"
+ android:background="@drawable/car_ui_toolbar_background"
+ android:orientation="horizontal">
+
+ <LinearLayout
+ android:id="@+id/browse_item_actions_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical|left"
+ android:layout_marginStart="@dimen/media_browse_header_action_margin" />
+
<TextView
android:id="@+id/title"
style="@style/BrowseSubheaderStyle"
- android:layout_width="match_parent"
+ android:layout_width="0dp"
android:layout_height="wrap_content"
- android:layout_gravity="center_vertical"
- android:includeFontPadding="false"/>
-</FrameLayout>
+ android:singleLine="true"
+ android:includeFontPadding="false"
+ android:layout_gravity="center_vertical|center_horizontal"
+ android:visibility="gone"/>
+</LinearLayout>
diff --git a/res/layout/media_browse_list_icons_item.xml b/res/layout/media_browse_list_icons_item.xml
index ae5419a..7216ffb 100644
--- a/res/layout/media_browse_list_icons_item.xml
+++ b/res/layout/media_browse_list_icons_item.xml
@@ -78,7 +78,7 @@
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"/>
+ app:layout_constraintEnd_toStartOf="@+id/text_end_guideline"/>
<ImageView
android:id="@+id/download_icon_with_subtitle"
@@ -111,8 +111,7 @@
app:layout_constraintTop_toBottomOf="@+id/title"
app:layout_constraintBottom_toBottomOf="@+id/browse_item_progress_bar"
app:layout_constraintStart_toEndOf="@+id/explicit_icon_with_subtitle"
- app:layout_constraintEnd_toStartOf="@+id/right_arrow"/>
-
+ app:layout_constraintEnd_toStartOf="@+id/text_end_guideline"/>
<ProgressBar
android:id="@+id/browse_item_progress_bar"
@@ -121,38 +120,59 @@
android:layout_height="@dimen/media_browse_progress_bar_height"
android:layout_marginBottom="@dimen/media_browse_progress_bar_bottom_list_margin"
android:layout_marginEnd="@dimen/media_browse_progress_bar_bottom_list_margin"
- app:layout_goneMarginEnd="@dimen/media_browse_progress_bar_gone_list_margin"
+ app:layout_constraintStart_toEndOf="@+id/explicit_icon_with_title"
+ app:layout_constraintEnd_toStartOf="@+id/text_end_guideline"
app:layout_constraintTop_toBottomOf="@+id/subtitle"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toEndOf="@+id/explicit_icon_with_title"
- app:layout_constraintEnd_toStartOf="@+id/browse_item_progress_new"
android:visibility="gone"/>
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/text_end_guideline"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ app:layout_constraintGuide_percent="0.75"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+
<ImageView
android:id="@+id/browse_item_progress_new"
android:src="@drawable/browser_progress_new_indicator"
+ android:layout_width="@dimen/media_browse_list_item_icons_size"
+ android:layout_height="@dimen/media_browse_list_item_icons_size"
+ android:layout_marginStart="@dimen/media_browse_list_item_icon_margin"
+ android:layout_marginEnd="@dimen/media_browse_list_item_icon_margin"
+ android:padding="@dimen/media_browse_list_item_icons_no_touch_padding"
+ android:scaleType="fitCenter"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/browse_item_actions_container"
+ android:visibility="gone"/>
+
+ <LinearLayout
+ android:id="@+id/browse_item_actions_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_margin="@dimen/media_browse_progress_indicator_margin"
- android:layout_gravity="center_vertical"
+ app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/right_arrow"
- app:layout_goneMarginEnd="@dimen/media_browse_progress_bar_gone_list_margin"
android:visibility="gone"/>
<ImageView
android:id="@+id/right_arrow"
style="@style/BrowseListItemRightArrowStyle"
- android:layout_width="@dimen/media_browse_list_item_arrow_size"
- android:layout_height="@dimen/media_browse_list_item_arrow_size"
+ android:layout_width="@dimen/media_browse_list_item_icons_size"
+ android:layout_height="@dimen/media_browse_list_item_icons_size"
android:layout_marginTop="@dimen/media_browse_subtitle_margin_top"
- android:layout_gravity="center_vertical"
- android:scaleType="centerCrop"
- android:includeFontPadding="false"
+ android:layout_marginStart="@dimen/media_browse_list_item_icon_margin"
+ android:layout_marginEnd="@dimen/media_browse_list_item_icon_margin"
+ android:scaleType="fitCenter"
+ app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toEndOf="@+id/title"/>
+ android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/res/layout/media_browse_list_item.xml b/res/layout/media_browse_list_item.xml
index 438353e..db2778d 100644
--- a/res/layout/media_browse_list_item.xml
+++ b/res/layout/media_browse_list_item.xml
@@ -76,7 +76,7 @@
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"/>
+ app:layout_constraintEnd_toStartOf="@+id/text_end_guideline"/>
<ImageView
android:id="@+id/download_icon_with_subtitle"
@@ -109,7 +109,7 @@
app:layout_constraintTop_toBottomOf="@+id/title"
app:layout_constraintBottom_toBottomOf="@+id/browse_item_progress_bar"
app:layout_constraintStart_toEndOf="@+id/explicit_icon_with_subtitle"
- app:layout_constraintEnd_toStartOf="@+id/right_arrow"/>
+ app:layout_constraintEnd_toStartOf="@+id/text_end_guideline"/>
<ProgressBar
android:id="@+id/browse_item_progress_bar"
@@ -118,38 +118,59 @@
android:layout_height="@dimen/media_browse_progress_bar_height"
android:layout_marginBottom="@dimen/media_browse_progress_bar_bottom_list_margin"
android:layout_marginEnd="@dimen/media_browse_progress_bar_bottom_list_margin"
- app:layout_goneMarginEnd="@dimen/media_browse_progress_bar_gone_list_margin"
+ app:layout_constraintStart_toEndOf="@+id/explicit_icon_with_title"
+ app:layout_constraintEnd_toStartOf="@+id/text_end_guideline"
app:layout_constraintTop_toBottomOf="@+id/subtitle"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toEndOf="@+id/explicit_icon_with_title"
- app:layout_constraintEnd_toStartOf="@+id/browse_item_progress_new"
android:visibility="gone"/>
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/text_end_guideline"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ app:layout_constraintGuide_percent="0.75"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+
<ImageView
android:id="@+id/browse_item_progress_new"
android:src="@drawable/browser_progress_new_indicator"
+ android:layout_width="@dimen/media_browse_list_item_icons_size"
+ android:layout_height="@dimen/media_browse_list_item_icons_size"
+ android:layout_marginStart="@dimen/media_browse_list_item_icon_margin"
+ android:layout_marginEnd="@dimen/media_browse_list_item_icon_margin"
+ android:padding="@dimen/media_browse_list_item_icons_no_touch_padding"
+ android:scaleType="fitCenter"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/browse_item_actions_container"
+ android:visibility="gone"/>
+
+ <LinearLayout
+ android:id="@+id/browse_item_actions_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_margin="@dimen/media_browse_progress_indicator_margin"
- android:layout_gravity="center_vertical"
+ app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/right_arrow"
- app:layout_goneMarginEnd="@dimen/media_browse_progress_indicator_gone_margin"
android:visibility="gone"/>
<ImageView
android:id="@+id/right_arrow"
style="@style/BrowseListItemRightArrowStyle"
- android:layout_width="@dimen/media_browse_list_item_arrow_size"
- android:layout_height="@dimen/media_browse_list_item_arrow_size"
+ android:layout_width="@dimen/media_browse_list_item_icons_size"
+ android:layout_height="@dimen/media_browse_list_item_icons_size"
android:layout_marginTop="@dimen/media_browse_subtitle_margin_top"
- android:layout_gravity="center_vertical"
- android:scaleType="centerCrop"
- android:includeFontPadding="false"
+ android:layout_marginStart="@dimen/media_browse_list_item_icon_margin"
+ android:layout_marginEnd="@dimen/media_browse_list_item_icon_margin"
+ android:scaleType="fitCenter"
+ app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toEndOf="@+id/title"/>
+ android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/res/layout/queue_list_item.xml b/res/layout/queue_list_item.xml
index 718e0b0..ec54b89 100644
--- a/res/layout/queue_list_item.xml
+++ b/res/layout/queue_list_item.xml
@@ -75,8 +75,8 @@
<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_width="@dimen/media_browse_list_item_icons_size"
+ android:layout_height="@dimen/media_browse_list_item_icons_size"
android:layout_gravity="center_vertical"
android:scaleType="centerCrop"/>
diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml
index 9254469..93b07fb 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -21,7 +21,7 @@
<string name="nothing_to_play" msgid="5916260606572129130">"Multimedia-edukia ez dago erabilgarri zerrenda honetan"</string>
<string name="cannot_connect_to_app" msgid="4732888036680095414">"Une honetan, <xliff:g id="ID_1">%s</xliff:g> aplikazioak ez du funtzionatzen."</string>
<string name="unknown_media_provider_name" msgid="4238216994694326667">"Ezezaguna"</string>
- <string name="unknown_error" msgid="6146463797752964372">"Arazo bat izan da"</string>
+ <string name="unknown_error" msgid="6146463797752964372">"Arazoren bat izan da"</string>
<string name="media_browse_more" msgid="6330295386693311592">"Gehiago…"</string>
<string name="media_app_title" msgid="94717597743776797">"Multimedia-edukia"</string>
<string name="search_hint" msgid="5401750426238148416">"Bilatu abestiak, artistak eta beste…"</string>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index 2a51973..1e83b73 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -17,7 +17,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="browser_loading" msgid="1639570085710372552">"Мазмұн жүктелуде…"</string>
+ <string name="browser_loading" msgid="1639570085710372552">"Контент жүктелуде…"</string>
<string name="nothing_to_play" msgid="5916260606572129130">"Бұл тізім үшін медиамазмұн жоқ."</string>
<string name="cannot_connect_to_app" msgid="4732888036680095414">"<xliff:g id="ID_1">%s</xliff:g> қазір жұмыс істемей тұр."</string>
<string name="unknown_media_provider_name" msgid="4238216994694326667">"Белгісіз"</string>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index 83e5b01..e58c1cf 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -27,6 +27,6 @@
<string name="search_hint" msgid="5401750426238148416">"Ырларды, аткаруучуларды издөө ж.б..."</string>
<string name="fragment_playback_title" msgid="5014481549024607614">"Эмне ойноп жатат?"</string>
<string name="service_notification_title" msgid="8085444675783592744">"Медиа булагына туташууда"</string>
- <string name="menu_item_sound_settings_title" msgid="58887078120809669">"Добуштун жөндөөлөрү"</string>
+ <string name="menu_item_sound_settings_title" msgid="58887078120809669">"Добуштун параметрлери"</string>
<string name="menu_item_app_selector_title" msgid="4587248991114338595">"Колдонмолорду которуштуруу"</string>
</resources>
diff --git a/res/values/bools.xml b/res/values/bools.xml
index 561a6a5..dd11ccd 100644
--- a/res/values/bools.xml
+++ b/res/values/bools.xml
@@ -44,4 +44,10 @@
<!-- Controls whether to show the tabs when the navigation button is visible. -->
<bool name="show_persistent_tabs">false</bool>
+
+ <!-- Whether the media source logo should be used for the app selector button in the playback
+ view. If the flag is set to 'true', the main logo (which by default appears on the left
+ hand side on toolbar) will be hidden. -->
+ <bool name="use_media_source_logo_for_app_selector_in_playback_view">false</bool>
+
</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 9dac304..aeb1af6 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -114,16 +114,19 @@
<!-- media_browse_header_item.xml -->
<dimen name="media_browse_header_item_height">76dp</dimen>
- <dimen name="media_browse_header_item_margin_x">0dp</dimen>
+ <dimen name="media_browse_header_action_margin">88dp</dimen>
<!-- media_browse_list_[icons_]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">148dp</dimen>
<dimen name="media_browse_list_item_icon_margin_start">0dp</dimen>
- <dimen name="media_browse_list_item_arrow_size">@dimen/car_ui_primary_icon_size</dimen>
+ <dimen name="media_browse_list_item_icon_margin">16dp</dimen>
+ <dimen name="media_browse_list_item_icons_no_touch_padding">12dp</dimen>
+ <dimen name="media_browse_list_item_icons_size">@dimen/car_ui_primary_icon_size</dimen>
<dimen name="media_browse_list_item_thumbnail_margin_bottom">4dp</dimen>
+
<dimen name="media_browse_list_icons_item_art_margin_start">@dimen/car_ui_padding_3</dimen>
<dimen name="media_browse_list_icons_item_text_margin_x">112dp</dimen>
<dimen name="media_browse_list_icons_item_art_size">@dimen/car_ui_primary_icon_size</dimen>
@@ -133,8 +136,6 @@
<!-- media browse playback progress bar -->
<dimen name="media_browse_progress_bar_height">12dp</dimen>
- <dimen name="media_browse_progress_bar_gone_list_margin">80dp</dimen>
- <dimen name="media_browse_progress_bar_gone_grid_margin">24dp</dimen>
<dimen name="media_browse_progress_bar_end_list_margin">16dp</dimen>
<dimen name="media_browse_progress_bar_end_grid_margin">8dp</dimen>
<dimen name="media_browse_progress_bar_top_list_margin">8dp</dimen>
@@ -142,8 +143,6 @@
<dimen name="media_browse_progress_bar_bottom_list_margin">8dp</dimen>
<dimen name="media_browse_progress_bar_bottom_grid_margin">8dp</dimen>
<dimen name="media_browse_progress_indicator_size">16dp</dimen>
- <dimen name="media_browse_progress_indicator_margin">8dp</dimen>
- <dimen name="media_browse_progress_indicator_gone_margin">52dp</dimen>
<!-- metadata_normal.xml -->
<dimen name="metadata_title_subtitle_margin">@dimen/car_ui_padding_2</dimen>
diff --git a/res/values/integers.xml b/res/values/integers.xml
index 2077b00..e2d86d5 100644
--- a/res/values/integers.xml
+++ b/res/values/integers.xml
@@ -49,7 +49,7 @@
<!-- Views to show when the queue is visible (to hide when the queue becomes invisible). -->
<integer-array name="playback_views_to_show_when_queue_is_visible">
- <item>@id/queue_list</item>
+ <item>@id/queue_fragment_container</item>
<item>@id/background_scrim</item>
</integer-array>
@@ -63,4 +63,12 @@
visual glitch. -->
<integer-array name="playback_views_to_show_immediately_when_queue_is_visible"/>
+ <!-- The maximum number of actions to show per item in the browse list view
+ before all items are in overflow -->
+ <integer name="max_visible_actions">1</integer>
+
+ <!-- The maximum number of actions to show in the actions header
+ Items beyond this limit will be in the overflow. -->
+ <integer name="max_visible_actions_header">2</integer>
+
</resources>
diff --git a/res/values/overlayable.xml b/res/values/overlayable.xml
index 16f53c5..211e368 100644
--- a/res/values/overlayable.xml
+++ b/res/values/overlayable.xml
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
-<!-- Copyright (C) 2022 The Android Open Source Project
+<!-- Copyright (C) 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
@@ -35,6 +35,7 @@
<item type="bool" name="show_time_for_now_playing_queue_list_item"/>
<item type="bool" name="switch_to_playback_view_when_playable_item_is_clicked"/>
<item type="bool" name="use_media_source_color_for_progress_bar"/>
+ <item type="bool" name="use_media_source_logo_for_app_selector_in_playback_view"/>
<item type="dimen" name="browse_fragment_bottom_padding"/>
<item type="dimen" name="browse_fragment_top_padding"/>
<item type="dimen" name="browse_fragment_top_padding_stacked"/>
@@ -59,6 +60,7 @@
<item type="drawable" name="ic_search"/>
<item type="drawable" name="ic_settings"/>
<item type="drawable" name="media_app_title_background"/>
+ <item type="drawable" name="media_item_divider"/>
<item type="drawable" name="music_overflow_action_background"/>
<item type="drawable" name="seekbar_foreground"/>
<item type="drawable" name="seekbar_progress"/>
@@ -68,6 +70,9 @@
<item type="id" name="artist"/>
<item type="id" name="background_scrim"/>
<item type="id" name="browse_content_area"/>
+ <item type="id" name="browse_item_actions_container"/>
+ <item type="id" name="browse_item_custom_action"/>
+ <item type="id" name="browse_item_custom_action_divider"/>
<item type="id" name="browse_item_progress_bar"/>
<item type="id" name="browse_item_progress_new"/>
<item type="id" name="browse_list"/>
@@ -117,10 +122,12 @@
<item type="id" name="queue_list_item_title"/>
<item type="id" name="queue_list_item_titles_container"/>
<item type="id" name="queue_list_top_constraint"/>
+ <item type="id" name="queue_fragment_container"/>
<item type="id" name="right_arrow"/>
<item type="id" name="separator"/>
<item type="id" name="spacer"/>
<item type="id" name="subtitle"/>
+ <item type="id" name="text_end_guideline"/>
<item type="id" name="text_start_guideline"/>
<item type="id" name="thumbnail"/>
<item type="id" name="thumbnail_container"/>
@@ -128,6 +135,7 @@
<item type="id" name="toast_error_container"/>
<item type="id" name="toast_error_icon"/>
<item type="id" name="toast_error_message"/>
+ <item type="id" name="toolbar_container"/>
<item type="id" name="ui_content_bottom_guideline"/>
<item type="id" name="ui_content_end_guideline"/>
<item type="id" name="ui_content_start_guideline"/>
@@ -135,6 +143,8 @@
<item type="id" name="ui_content_top_guideline2"/>
<item type="integer" name="fragment_playback_queue_fade_duration_ms"/>
<item type="integer" name="max_tabs"/>
+ <item type="integer" name="max_visible_actions"/>
+ <item type="integer" name="max_visible_actions_header"/>
<item type="integer" name="media_artist_max_lines"/>
<item type="integer" name="media_title_max_lines"/>
<item type="integer" name="num_app_bar_view_rows"/>
@@ -142,6 +152,7 @@
<item type="integer" name="progress_indicator_delay"/>
<item type="interpolator" name="trim_end_interpolator"/>
<item type="interpolator" name="trim_start_interpolator"/>
+ <item type="layout" name="browse_custom_action"/>
<item type="layout" name="browse_mini_bar"/>
<item type="layout" name="browse_mini_bar_container"/>
<item type="layout" name="browse_mini_bar_view"/>
@@ -149,6 +160,7 @@
<item type="layout" name="fragment_browse"/>
<item type="layout" name="fragment_error"/>
<item type="layout" name="fragment_playback"/>
+ <item type="layout" name="fragment_playback_queue"/>
<item type="layout" name="media_activity"/>
<item type="layout" name="media_browse_grid_icons_item"/>
<item type="layout" name="media_browse_grid_item"/>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 6691007..9d4682a 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -90,7 +90,7 @@
</style>
<style name="BrowseListItemRightArrowStyle">
- <item name="android:src">@null</item>
+ <item name="android:src">@drawable/ic_chevron_right</item>
</style>
<style name="MediaIconContainerStyle"/>
diff --git a/res/xml/menuitems_playback.xml b/res/xml/menuitems_playback.xml
index 1b3444c..d7284b5 100644
--- a/res/xml/menuitems_playback.xml
+++ b/res/xml/menuitems_playback.xml
@@ -20,4 +20,15 @@
app:id="@+id/menu_item_queue"
app:carUiIcon="@drawable/ic_queue_button"
app:activatable="true"/>
+ <MenuItem
+ app:id="@+id/menu_item_selector_with_source_logo"
+ app:title="@string/menu_item_app_selector_title"
+ app:tinted="false"/>
+ <!-- Uncomment or overlay to add the app selector to the Now Playing view.
+ <MenuItem
+ app:id="@+id/menu_item_selector"
+ app:title="@string/menu_item_app_selector_title"
+ app:carUiIcon="@drawable/ic_app_switch"
+ app:tinted="true"/>
+ -->
</MenuItems>
diff --git a/src/com/android/car/media/BrowseViewController.java b/src/com/android/car/media/BrowseViewController.java
index 57fefbe..43f97c0 100644
--- a/src/com/android/car/media/BrowseViewController.java
+++ b/src/com/android/car/media/BrowseViewController.java
@@ -20,11 +20,18 @@
import static android.car.media.CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK;
import static com.android.car.apps.common.util.ViewUtils.removeFromParent;
+import static com.android.car.media.common.MediaConstants.BROWSE_CUSTOM_ACTIONS_MEDIA_ITEM_ID;
import static com.android.car.ui.recyclerview.CarUiRecyclerView.SCROLL_STATE_DRAGGING;
+import android.app.AlertDialog;
import android.content.res.Resources;
+import android.os.Bundle;
import android.os.Handler;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaBrowserCompat.ItemCallback;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
import android.support.v4.media.session.PlaybackStateCompat;
+import android.text.TextUtils;
import android.util.Log;
import android.util.Size;
import android.view.LayoutInflater;
@@ -32,37 +39,53 @@
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
+import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.RecyclerView;
+import com.android.car.apps.common.imaging.ImageBinder;
import com.android.car.apps.common.util.FutureData;
import com.android.car.apps.common.util.LiveDataFunctions;
import com.android.car.apps.common.util.ViewUtils;
import com.android.car.media.browse.BrowseAdapter;
+import com.android.car.media.browse.BrowseAdapterUtils;
import com.android.car.media.browse.BrowseMiniMediaItemView;
import com.android.car.media.browse.BrowseViewHolder;
import com.android.car.media.browse.LimitedBrowseAdapter;
+import com.android.car.media.browse.actionbar.ActionsHeader;
+import com.android.car.media.common.CustomBrowseAction;
+import com.android.car.media.common.MediaConstants;
import com.android.car.media.common.MediaItemMetadata;
import com.android.car.media.common.browse.MediaBrowserViewModelImpl;
+import com.android.car.media.common.browse.MediaItemsRepository;
import com.android.car.media.common.browse.MediaItemsRepository.MediaItemsLiveData;
import com.android.car.media.common.playback.PlaybackProgress;
import com.android.car.media.common.playback.PlaybackViewModel;
+import com.android.car.media.common.source.MediaBrowserConnector.BrowsingState;
import com.android.car.media.common.source.MediaSource;
+import com.android.car.ui.AlertDialogBuilder;
import com.android.car.ui.FocusArea;
import com.android.car.ui.baselayout.Insets;
+import com.android.car.ui.recyclerview.CarUiContentListItem;
+import com.android.car.ui.recyclerview.CarUiListItemAdapter;
import com.android.car.ui.recyclerview.CarUiRecyclerView;
import com.android.car.uxr.LifeCycleObserverUxrContentLimiter;
import com.android.car.uxr.UxrContentLimiterImpl;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
/**
@@ -78,6 +101,7 @@
private final Callbacks mCallbacks;
private final FocusArea mFocusArea;
private final MediaItemMetadata mParentItem;
+ private List<CustomBrowseAction> mParentActions;
private final MediaItemsLiveData mMediaItems;
private final boolean mDisplayMediaItems;
private final LifeCycleObserverUxrContentLimiter mUxrContentLimiter;
@@ -99,9 +123,12 @@
private final PlaybackViewModel mPlaybackViewModel;
private final PlaybackViewModel mPlaybackViewModelBrowseSource;
+ private MediaItemsRepository mMediaRepo;
+ private Map<String, CustomBrowseAction> mGlobalActions = new HashMap<>();
+
+ private ActionsHeader mActionBar;
private final BrowseAdapter.Observer mBrowseAdapterObserver = new BrowseAdapter.Observer() {
-
@Override
protected void onPlayableItemClicked(@NonNull MediaItemMetadata item) {
mCallbacks.onPlayableItemClicked(item);
@@ -111,6 +138,59 @@
protected void onBrowsableItemClicked(@NonNull MediaItemMetadata item) {
mCallbacks.onBrowsableItemClicked(item);
}
+
+ @Override
+ protected void onBrowseCustomActionClicked(
+ @NonNull CustomBrowseAction customBrowseAction, String mediaId) {
+ sendBrowseCustomAction(customBrowseAction, mediaId);
+ }
+
+ @Override
+ protected void onBrowseCustomActionOverflowClicked(
+ @NonNull List<CustomBrowseAction> overflowActions, String mediaId) {
+ showOverflowActions(overflowActions, mediaId);
+ }
+ };
+
+ /** Callback from browse service for custom actions */
+ public static class CustomActionCallback extends MediaBrowserCompat.CustomActionCallback {
+
+ WeakReference<BrowseViewController> mBrowseViewControllerWeakReference;
+
+ public CustomActionCallback(BrowseViewController browseViewController) {
+ this.mBrowseViewControllerWeakReference = new WeakReference<>(browseViewController);
+ }
+
+ @Override
+ public void onProgressUpdate(String action, Bundle extras, Bundle resultData) {
+ BrowseViewController bvc = mBrowseViewControllerWeakReference.get();
+ if (bvc != null) {
+ bvc.handleBrowseCustomActionResult(action, extras, resultData);
+ }
+ }
+
+ @Override
+ public void onResult(String action, Bundle extras, Bundle resultData) {
+ BrowseViewController bvc = mBrowseViewControllerWeakReference.get();
+ if (bvc != null) {
+ bvc.handleBrowseCustomActionResult(action, extras, resultData);
+ }
+ }
+
+ @Override
+ public void onError(String action, Bundle extras, Bundle resultData) {
+ Log.e(TAG, "CustomActionCallback onError: " + action);
+ BrowseViewController bvc = mBrowseViewControllerWeakReference.get();
+ if (bvc != null) {
+ if (resultData.containsKey(
+ MediaConstants.BROWSE_CUSTOM_ACTIONS_EXTRA_RESULT_MESSAGE)) {
+ String text =
+ resultData.getString(
+ MediaConstants.BROWSE_CUSTOM_ACTIONS_EXTRA_RESULT_MESSAGE);
+ Toast.makeText(bvc.mContent.getContext(), text, Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
};
/**
@@ -141,6 +221,11 @@
*/
void onBrowseEmptyListPlayItemClicked();
+ /**
+ * Opens Playback view without starting new content.
+ */
+ void openPlaybackView();
+
/** Invoked when child nodes have been removed from this controller. */
void onChildrenNodesRemoved(@NonNull BrowseViewController controller,
@NonNull Collection<MediaItemMetadata> removedNodes);
@@ -160,17 +245,28 @@
* This parent node can have been obtained from the browse tree, or from browsing the search
* results.
*/
- static BrowseViewController newBrowseController(Callbacks callbacks, ViewGroup container,
- @NonNull MediaItemMetadata parentItem, MediaItemsLiveData mediaItems,
- int rootBrowsableHint, int rootPlayableHint) {
+ static BrowseViewController newBrowseController(
+ Callbacks callbacks,
+ ViewGroup container,
+ @NonNull MediaItemMetadata parentItem,
+ MediaItemsLiveData mediaItems,
+ MediaItemsRepository mediaRepo,
+ MutableLiveData<Map<String, CustomBrowseAction>> globalBrowseActions,
+ int rootBrowsableHint,
+ int rootPlayableHint) {
return new BrowseViewController(callbacks, container, parentItem, mediaItems,
- rootBrowsableHint, rootPlayableHint, true);
+ rootBrowsableHint, rootPlayableHint, mediaRepo, globalBrowseActions, true);
}
/** Creates a controller to display the top results of a search query (in a list). */
- static BrowseViewController newSearchResultsController(Callbacks callbacks, ViewGroup container,
- MediaItemsLiveData mediaItems) {
- return new BrowseViewController(callbacks, container, null, mediaItems, 0, 0, true);
+ static BrowseViewController newSearchResultsController(
+ Callbacks callbacks,
+ ViewGroup container,
+ MediaItemsLiveData mediaItems,
+ MediaItemsRepository mediaRepo,
+ MutableLiveData<Map<String, CustomBrowseAction>> globalBrowseActions) {
+ return new BrowseViewController(
+ callbacks, container, null, mediaItems, 0, 0, mediaRepo, globalBrowseActions, true);
}
/**
@@ -178,9 +274,14 @@
* since they are shown as tabs, and the controller is only used to display loading and error
* messages.
*/
- static BrowseViewController newRootController(Callbacks callbacks, ViewGroup container,
- MediaItemsLiveData mediaItems) {
- return new BrowseViewController(callbacks, container, null, mediaItems, 0, 0, false);
+ static BrowseViewController newRootController(
+ Callbacks callbacks,
+ ViewGroup container,
+ MediaItemsLiveData mediaItems,
+ MediaItemsRepository mediaRepo,
+ MutableLiveData<Map<String, CustomBrowseAction>> globalBrowseActions) {
+ return new BrowseViewController(callbacks, container, null, mediaItems, 0, 0, mediaRepo,
+ globalBrowseActions, false);
}
/**
@@ -217,19 +318,45 @@
}
}
- private BrowseViewController(Callbacks callbacks, ViewGroup container,
- @Nullable MediaItemMetadata parentItem, MediaItemsLiveData mediaItems,
- int rootBrowsableHint, int rootPlayableHint, boolean displayMediaItems) {
+ private abstract static class BrowseActionCallback extends ItemCallback{
+ @Override
+ public abstract void onItemLoaded(MediaItem item);
+
+ @Override
+ public void onError(@NonNull String itemId) {
+ super.onError(itemId);
+ Log.e(TAG, "BrowseActionCallback#onError -> " + itemId);
+ }
+ }
+
+ private BrowseViewController(
+ Callbacks callbacks,
+ ViewGroup container,
+ @Nullable MediaItemMetadata parentItem,
+ MediaItemsLiveData mediaItems,
+ int rootBrowsableHint,
+ int rootPlayableHint,
+ MediaItemsRepository mediaRepo,
+ MutableLiveData<Map<String, CustomBrowseAction>> globalBrowseActions,
+ boolean displayMediaItems) {
mCallbacks = callbacks;
mParentItem = parentItem;
mMediaItems = mediaItems;
mDisplayMediaItems = displayMediaItems;
+ mMediaRepo = mediaRepo;
+
+ FragmentActivity activity = callbacks.getActivity();
+ mViewModel = ViewModelProviders.of(activity).get(MediaActivity.ViewModel.class);
LayoutInflater inflater = LayoutInflater.from(container.getContext());
mContent = inflater.inflate(R.layout.browse_node, container, false);
mContent.setAlpha(0f);
container.addView(mContent);
+ int maxActions = mContent.getContext().getResources()
+ .getInteger(com.android.car.media.common.R.integer.max_custom_actions);
+ initCustomActionsHeader(parentItem, maxActions);
+
Resources res = mContent.getContext().getResources();
mLoadingIndicatorDelay = res.getInteger(R.integer.progress_indicator_delay);
mSetFocusAreaHighlightBottom = res.getBoolean(
@@ -242,9 +369,6 @@
mFadeDuration = mContent.getContext().getResources().getInteger(
R.integer.new_album_art_fade_in_duration);
- FragmentActivity activity = callbacks.getActivity();
- mViewModel = ViewModelProviders.of(activity).get(MediaActivity.ViewModel.class);
-
mPlaybackViewModel = PlaybackViewModel.get(activity.getApplication(),
MEDIA_SOURCE_MODE_PLAYBACK);
mPlaybackViewModelBrowseSource = PlaybackViewModel.get(activity.getApplication(),
@@ -288,17 +412,183 @@
mUxrContentLimiter.setAdapter(mLimitedBrowseAdapter);
activity.getLifecycle().addObserver(mUxrContentLimiter);
+ globalBrowseActions.observe(activity, actions -> {
+ mGlobalActions = actions;
+ browseAdapter.setGlobalCustomActions(actions);
+ configureCustomActionsHeader(actions, maxActions);
+ });
+
browseAdapter.setRootBrowsableViewType(rootBrowsableHint);
browseAdapter.setRootPlayableViewType(rootPlayableHint);
-
+ browseAdapter.setGlobalCustomActions(mGlobalActions);
mMediaItems.observe(activity, mItemsObserver);
}
+ private void initCustomActionsHeader(MediaItemMetadata parentItem, int maxActions) {
+ if (parentItem == null || maxActions <= 0) {
+ return;
+ }
+ mActionBar = mContent.findViewById(R.id.toolbar_container);
+ mActionBar.setActionClickedListener(
+ action -> sendBrowseCustomAction(action, parentItem.getId()));
+ mActionBar.setOnOverflowListener(
+ actions -> showOverflowActions(actions, parentItem.getId()));
+ }
+
+ private void configureCustomActionsHeader(
+ @NonNull Map<String, CustomBrowseAction> globalActions, int maxActions) {
+ if (mActionBar == null || maxActions <= 0) return;
+ mParentActions =
+ BrowseAdapterUtils.buildBrowseCustomActions(
+ mContent.getContext(), mParentItem, globalActions);
+ if (mParentActions == null || mParentActions.isEmpty()) return;
+ mActionBar.setVisibility(true);
+ mActionBar.setActions(mParentActions);
+ }
+
+ private void sendBrowseCustomAction(CustomBrowseAction customBrowseAction, String mediaItemId) {
+ final BrowsingState browsingState = mMediaRepo.getBrowsingState().getValue();
+ if (browsingState != null) {
+ final MediaBrowserCompat mediaBrowserCompat = browsingState.mBrowser;
+ Bundle extras = new Bundle();
+ //We need to pass this to browse service in order for them to properly handle action
+ extras.putString(BROWSE_CUSTOM_ACTIONS_MEDIA_ITEM_ID, mediaItemId);
+ mediaBrowserCompat.sendCustomAction(
+ customBrowseAction.getId(), extras, new CustomActionCallback(this));
+ }
+ }
+
+ private void showOverflowActions(List<CustomBrowseAction> overflowActions, String mediaId) {
+ final Size mMaxArtSize =
+ MediaAppConfig.getMediaItemsBitmapMaxSize(mContent.getContext());
+
+ List<CarUiContentListItem> data = new ArrayList<>();
+ CarUiListItemAdapter adapter = new CarUiListItemAdapter(data);
+ AlertDialog dialog =
+ new AlertDialogBuilder(mContent.getContext())
+ .setAdapter(adapter)
+ .setCancelable(true)
+ .create();
+
+ for (CustomBrowseAction customBrowseAction : overflowActions) {
+ CarUiContentListItem item = new CarUiContentListItem(CarUiContentListItem.Action.ICON);
+ item.setPrimaryIconType(CarUiContentListItem.IconType.AVATAR);
+ item.setTitle(customBrowseAction.getLabel());
+ ImageBinder<CustomBrowseAction.BrowseActionArtRef> imageBinder =
+ new ImageBinder<>(
+ ImageBinder.PlaceholderType.FOREGROUND,
+ mMaxArtSize,
+ drawable -> {
+ item.setIcon(drawable);
+ adapter.notifyDataSetChanged();
+ });
+ imageBinder.setImage(mContent.getContext(), customBrowseAction.getArtRef());
+ item.setOnItemClickedListener(
+ (contentItem) -> {
+ sendBrowseCustomAction(customBrowseAction, mediaId);
+ dialog.dismiss();
+ });
+ data.add(item);
+ }
+ dialog.show();
+ }
+
+ private boolean handleBrowseCustomActionsExtras(Bundle actionExtras) {
+ boolean handled = false;
+
+ if (actionExtras.containsKey(
+ MediaConstants.BROWSE_CUSTOM_ACTIONS_EXTRA_RESULT_MESSAGE)) {
+ handled = true;
+ String text = actionExtras.getString(
+ MediaConstants.BROWSE_CUSTOM_ACTIONS_EXTRA_RESULT_MESSAGE);
+ Toast.makeText(
+ getContent().getContext(),
+ text,
+ Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ if (actionExtras.containsKey(
+ MediaConstants.BROWSE_CUSTOM_ACTIONS_EXTRA_RESULT_OPEN_PLAYBACK)) {
+ handled = true;
+ mCallbacks.openPlaybackView();
+ }
+
+ if (actionExtras.containsKey(
+ MediaConstants.BROWSE_CUSTOM_ACTIONS_EXTRA_RESULT_BROWSE_NODE)) {
+ String mediaItemId =
+ actionExtras.getString(
+ MediaConstants.BROWSE_CUSTOM_ACTIONS_EXTRA_RESULT_BROWSE_NODE);
+ if (!TextUtils.isEmpty(mediaItemId)) {
+ handled = true;
+ mMediaRepo.getItem(
+ mediaItemId,
+ new BrowseActionCallback() {
+ @Override
+ public void onItemLoaded(MediaItem item) {
+ mCallbacks.onBrowsableItemClicked(new MediaItemMetadata(item));
+ }
+ });
+ }
+ }
+
+ if (actionExtras.containsKey(
+ MediaConstants.BROWSE_CUSTOM_ACTIONS_EXTRA_RESULT_REFRESH_ITEM)) {
+ String mediaItemId =
+ actionExtras.getString(
+ MediaConstants.BROWSE_CUSTOM_ACTIONS_EXTRA_RESULT_REFRESH_ITEM);
+ if (!TextUtils.isEmpty(mediaItemId)) {
+ handled = true;
+ mMediaRepo.getItem(
+ mediaItemId,
+ new BrowseActionCallback() {
+ @Override
+ public void onItemLoaded(MediaItem item) {
+ handleActionItemRefreshed(item);
+ }
+ });
+ }
+ }
+ return handled;
+ }
+
+ private void handleActionItemRefreshed(MediaItem item) {
+ if (Objects.equals(item.getDescription().getMediaId(), mParentItem.getId())) {
+ mParentActions =
+ BrowseAdapterUtils.buildBrowseCustomActions(
+ mContent.getContext(),
+ new MediaItemMetadata(item),
+ mGlobalActions);
+ mActionBar.setActions(mParentActions);
+ } else {
+ mLimitedBrowseAdapter.updateItemMetaData(
+ new MediaItemMetadata(item),
+ BrowseAdapter.MediaItemUpdateType.BROWSE_ACTIONS);
+ }
+ }
+
+ /**
+ * @param action - action that was invoked
+ * @param extras - Sent to client
+ * @param resultData - Returned from Client
+ */
+ private void handleBrowseCustomActionResult(String action, Bundle extras, Bundle resultData) {
+ if (!handleBrowseCustomActionsExtras(resultData)) {
+ Log.v(TAG, "Unhandled Action Result: " + action);
+ }
+ String mediaItemId = extras.getString(MediaConstants.BROWSE_CUSTOM_ACTIONS_MEDIA_ITEM_ID);
+ Log.v(TAG, String.format("Action Result: %s from item: %s", action, mediaItemId));
+ }
+
private void handleSourceUpdates(Pair<MediaSource, MediaSource> mediaSourceMediaSourcePair) {
- //If sources are the same, make sure we aren't showing the mini item bar.
- if (Objects.equals(mediaSourceMediaSourcePair.first, mediaSourceMediaSourcePair.second)) {
+ // If sources are the same, make sure we aren't showing the mini item bar.
+ if (isSourcesSame()) {
hideEmptyListPlayItem();
}
+ if (mediaSourceMediaSourcePair.second != null && mActionBar != null) {
+ CharSequence browseSourceName = mediaSourceMediaSourcePair.second.getDisplayName();
+ mActionBar.setTitle(browseSourceName);
+ }
}
public MediaItemMetadata getParentItem() {
@@ -375,16 +665,22 @@
}
public void onCarUiInsetsChanged(@NonNull Insets insets) {
+ int actionHeaderOffset = 0;
+ if (mActionBar != null && mActionBar.isShown()) {
+ Resources res = getActivity().getResources();
+ actionHeaderOffset = res.getDimensionPixelSize(R.dimen.media_browse_header_item_height);
+ }
int leftPadding = mBrowseList.getPaddingLeft();
int rightPadding = mBrowseList.getPaddingRight();
- int bottomPadding = mBrowseList.getPaddingBottom();
- mBrowseList.setPadding(leftPadding, insets.getTop(), rightPadding, bottomPadding);
+ int bottomPadding = mBrowseList.getPaddingBottom() + actionHeaderOffset;
+ int topPadding = insets.getTop() + actionHeaderOffset;
+ mBrowseList.setPadding(leftPadding, topPadding, rightPadding, bottomPadding);
if (bottomPadding > mFocusAreaHighlightBottomPadding) {
mFocusAreaHighlightBottomPadding = bottomPadding;
}
mFocusArea.setHighlightPadding(
- leftPadding, insets.getTop(), rightPadding, mFocusAreaHighlightBottomPadding);
- mFocusArea.setBoundsOffset(leftPadding, insets.getTop(), rightPadding, bottomPadding);
+ leftPadding, topPadding, rightPadding, mFocusAreaHighlightBottomPadding);
+ mFocusArea.setBoundsOffset(leftPadding, topPadding, rightPadding, bottomPadding);
}
void onPlaybackControlsChanged(boolean visible) {
@@ -557,7 +853,8 @@
if (adapterMetaData != null) {
double progress = progressMetaPair.first.getProgressFraction();
adapterMetaData.setProgress(progress);
- mLimitedBrowseAdapter.updateItemMetaData(adapterMetaData);
+ mLimitedBrowseAdapter.updateItemMetaData(adapterMetaData,
+ BrowseAdapter.MediaItemUpdateType.PROGRESS);
}
} else {
// Ignore, playback app is not the same as browse app, therefore no UI update needed.
diff --git a/src/com/android/car/media/GuidelinesUpdater.java b/src/com/android/car/media/GuidelinesUpdater.java
index 1919901..1af4a66 100644
--- a/src/com/android/car/media/GuidelinesUpdater.java
+++ b/src/com/android/car/media/GuidelinesUpdater.java
@@ -31,7 +31,7 @@
* Applies the insets computed by the car-ui-lib to the spacer views in ui_guides.xml. This allows
* the Media app to have different instances of the application bar.
*/
-class GuidelinesUpdater implements InsetsChangedListener {
+public class GuidelinesUpdater implements InsetsChangedListener {
private final View mGuidedView;
private final Set<InsetsChangedListener> mListeners = new HashSet<>();
diff --git a/src/com/android/car/media/MediaActivity.java b/src/com/android/car/media/MediaActivity.java
index dd5438c..d5c962a 100644
--- a/src/com/android/car/media/MediaActivity.java
+++ b/src/com/android/car/media/MediaActivity.java
@@ -332,7 +332,7 @@
if (intent != null && !isUxRestricted()) {
maybeCancelDialog();
showDialog(intent, displayedMessage, label,
- getString(android.R.string.cancel), icon);
+ getString(android.R.string.cancel), icon, mediaSource);
} else {
maybeCancelToast();
showToast(displayedMessage, icon);
@@ -349,11 +349,15 @@
return mErrorController;
}
- private void showDialog(PendingIntent intent, String message, String positiveBtnText,
- String negativeButtonText, @Nullable Drawable icon) {
+ private void showDialog(
+ PendingIntent intent,
+ String message,
+ String positiveBtnText,
+ String negativeButtonText,
+ @Nullable Drawable icon,
+ MediaSource mediaSource) {
boolean showTitleIcon = getResources().getBoolean(R.bool.show_playback_source_id);
- String title = getPlaybackViewModel(
- MEDIA_SOURCE_MODE_PLAYBACK).getMediaSource().getValue().getDisplayName().toString();
+ String title = mediaSource != null ? mediaSource.getDisplayName().toString() : "";
AlertDialogBuilder dialog = new AlertDialogBuilder(this);
mDialog = dialog.setMessage(message)
@@ -373,7 +377,7 @@
}
private void showToast(String message, @Nullable Drawable icon) {
- mToast = Toast.makeText(this.getApplicationContext(), message, Toast.LENGTH_LONG);
+ mToast = Toast.makeText(this, message, Toast.LENGTH_LONG);
int offset = getResources().getDimensionPixelOffset(R.dimen.toast_error_offset_y);
mToast.setGravity(Gravity.BOTTOM, 0, offset);
@@ -561,17 +565,21 @@
@Override
public void onPlayableItemClicked(@NonNull MediaItemMetadata item) {
mBrowsePlaybackController.playItem(item);
- boolean switchToPlayback = getResources().getBoolean(
- R.bool.switch_to_playback_view_when_playable_item_is_clicked);
- if (switchToPlayback) {
- changeMode(Mode.PLAYBACK);
- }
- setIntent(null);
+ maybeOpenPlayback();
}
@Override
public void onBrowseEmptyListPlayItemClicked() {
mBrowsePlaybackController.play();
+ maybeOpenPlayback();
+ }
+
+ @Override
+ public void openPlaybackView() {
+ maybeOpenPlayback();
+ }
+
+ private void maybeOpenPlayback() {
boolean switchToPlayback = getResources().getBoolean(
R.bool.switch_to_playback_view_when_playable_item_is_clicked);
if (switchToPlayback) {
diff --git a/src/com/android/car/media/MediaActivityController.java b/src/com/android/car/media/MediaActivityController.java
index 27561fd..63b512a 100644
--- a/src/com/android/car/media/MediaActivityController.java
+++ b/src/com/android/car/media/MediaActivityController.java
@@ -32,6 +32,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.LinearLayoutManager;
@@ -40,6 +41,7 @@
import com.android.car.apps.common.util.FutureData;
import com.android.car.apps.common.util.ViewUtils;
import com.android.car.apps.common.util.ViewUtils.ViewAnimEndListener;
+import com.android.car.media.common.CustomBrowseAction;
import com.android.car.media.common.MediaItemMetadata;
import com.android.car.media.common.browse.MediaBrowserViewModelImpl;
import com.android.car.media.common.browse.MediaItemsRepository;
@@ -127,6 +129,9 @@
/** Called once the list of the root node's children has been loaded. */
void onRootLoaded();
+ /** Called when switching to pbv without changing playback content*/
+ void openPlaybackView();
+
/** Returns the activity. */
FragmentActivity getActivity();
}
@@ -250,12 +255,24 @@
mFpv = activity.requireViewById(R.id.fpv);
MediaItemsLiveData rootMediaItems = mediaItemsRepo.getRootMediaItems();
- mRootLoadingController = BrowseViewController.newRootController(
- mBrowseCallbacks, mBrowseArea, rootMediaItems);
+ MutableLiveData<Map<String, CustomBrowseAction>> globalBrowseActions =
+ mediaItemsRepo.getCustomBrowseActions();
+ mRootLoadingController =
+ BrowseViewController.newRootController(
+ mBrowseCallbacks,
+ mBrowseArea,
+ rootMediaItems,
+ mediaItemsRepo,
+ globalBrowseActions);
mRootLoadingController.getContent().setAlpha(1f);
- mSearchResultsController = BrowseViewController.newSearchResultsController(
- mBrowseCallbacks, mBrowseArea, mMediaItemsRepository.getSearchMediaItems());
+ mSearchResultsController =
+ BrowseViewController.newSearchResultsController(
+ mBrowseCallbacks,
+ mBrowseArea,
+ mMediaItemsRepository.getSearchMediaItems(),
+ mediaItemsRepo,
+ globalBrowseActions);
boolean showingSearch = mViewModel.isShowingSearchResults();
ViewUtils.setVisible(mSearchResultsController.getContent(), showingSearch);
@@ -353,6 +370,12 @@
}
@Override
+ public void openPlaybackView() {
+ hideKeyboard();
+ mCallbacks.openPlaybackView();
+ }
+
+ @Override
public void onChildrenNodesRemoved(@NonNull BrowseViewController controller,
@NonNull Collection<MediaItemMetadata> removedNodes) {
if (mBrowseStack.contains(controller.getParentItem())) {
@@ -425,10 +448,19 @@
@NonNull
private BrowseViewController getControllerForItem(@NonNull MediaItemMetadata item) {
BrowseViewController controller = mBrowseViewControllersByNode.get(item);
+ MutableLiveData<Map<String, CustomBrowseAction>> globalBrowseActions =
+ mMediaItemsRepository.getCustomBrowseActions();
if (controller == null) {
- controller = BrowseViewController.newBrowseController(mBrowseCallbacks, mBrowseArea,
- item, mMediaItemsRepository.getMediaChildren(item.getId()), mRootBrowsableHint,
- mRootPlayableHint);
+ controller =
+ BrowseViewController.newBrowseController(
+ mBrowseCallbacks,
+ mBrowseArea,
+ item,
+ mMediaItemsRepository.getMediaChildren(item.getId()),
+ mMediaItemsRepository,
+ globalBrowseActions,
+ mRootBrowsableHint,
+ mRootPlayableHint);
if (mCarUiInsets != null) {
controller.onCarUiInsetsChanged(mCarUiInsets);
diff --git a/src/com/android/car/media/MediaAppConfig.java b/src/com/android/car/media/MediaAppConfig.java
index d519b51..22f0ad9 100644
--- a/src/com/android/car/media/MediaAppConfig.java
+++ b/src/com/android/car/media/MediaAppConfig.java
@@ -35,4 +35,5 @@
com.android.car.media.common.R.integer.media_items_bitmap_max_size_px);
return new Size(max, max);
}
+
}
diff --git a/src/com/android/car/media/PlaybackFragment.java b/src/com/android/car/media/PlaybackFragment.java
index 20fedc4..438f739 100644
--- a/src/com/android/car/media/PlaybackFragment.java
+++ b/src/com/android/car/media/PlaybackFragment.java
@@ -22,12 +22,10 @@
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.PorterDuff;
-import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Bundle;
-import android.util.Log;
import android.util.Size;
import android.view.LayoutInflater;
import android.view.View;
@@ -38,16 +36,15 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.util.Preconditions;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
-import androidx.recyclerview.widget.DefaultItemAnimator;
-import androidx.recyclerview.widget.RecyclerView;
import com.android.car.apps.common.BackgroundImageView;
import com.android.car.apps.common.imaging.ImageBinder;
import com.android.car.apps.common.imaging.ImageBinder.PlaceholderType;
-import com.android.car.apps.common.imaging.ImageViewBinder;
import com.android.car.apps.common.util.ViewUtils;
+import com.android.car.media.PlaybackQueueFragment.PlaybackQueueCallback;
import com.android.car.media.common.MediaItemMetadata;
import com.android.car.media.common.MetadataController;
import com.android.car.media.common.PlaybackControlsActionBar;
@@ -55,21 +52,13 @@
import com.android.car.media.common.source.MediaSourceViewModel;
import com.android.car.media.widgets.AppBarController;
import com.android.car.ui.core.CarUi;
-import com.android.car.ui.recyclerview.CarUiRecyclerView;
-import com.android.car.ui.recyclerview.ContentLimiting;
-import com.android.car.ui.recyclerview.ScrollingLimitedViewHolder;
import com.android.car.ui.toolbar.MenuItem;
-import com.android.car.ui.toolbar.MenuItemXmlParserUtil;
import com.android.car.ui.toolbar.NavButtonMode;
import com.android.car.ui.toolbar.ToolbarController;
import com.android.car.ui.utils.DirectManipulationHelper;
import com.android.car.uxr.LifeCycleObserverUxrContentLimiter;
-import com.android.car.uxr.UxrContentLimiterImpl;
-import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
-import java.util.Objects;
/**
@@ -84,12 +73,11 @@
private ImageBinder<MediaItemMetadata.ArtworkRef> mAlbumArtBinder;
private AppBarController mAppBarController;
private BackgroundImageView mAlbumBackground;
+ private PlaybackQueueFragment mPlaybackQueueFragment;
private View mBackgroundScrim;
private View mControlBarScrim;
private PlaybackControlsActionBar mPlaybackControls;
private PlaybackViewModel mPlaybackViewModel;
- private QueueItemsAdapter mQueueAdapter;
- private CarUiRecyclerView mQueue;
private ViewGroup mSeekBarContainer;
private SeekBar mSeekBar;
private List<View> mViewsToHideForCustomActions;
@@ -98,21 +86,12 @@
private List<View> mViewsToHideImmediatelyWhenQueueIsVisible;
private List<View> mViewsToShowImmediatelyWhenQueueIsVisible;
- private DefaultItemAnimator mItemAnimator;
-
private MetadataController mMetadataController;
private PlaybackFragmentListener mListener;
- private PlaybackViewModel.PlaybackController mController;
- private Long mActiveQueueItemId;
-
private boolean mHasQueue;
private boolean mQueueIsVisible;
- private boolean mShowTimeForActiveQueueItem;
- private boolean mShowIconForActiveQueueItem;
- private boolean mShowThumbnailForQueueItem;
- private boolean mShowSubtitleForQueueItem;
private boolean mShowLinearProgressBar;
@@ -122,6 +101,18 @@
private MenuItem mQueueMenuItem;
+ private PlaybackQueueFragment.PlaybackQueueCallback mPlaybackQueueCallback =
+ new PlaybackQueueCallback() {
+ @Override
+ public void onQueueItemClicked(MediaItemMetadata item) {
+ boolean switchToPlayback = getResources().getBoolean(
+ R.bool.switch_to_playback_view_when_playable_item_is_clicked);
+ if (switchToPlayback) {
+ toggleQueueVisibility();
+ }
+ }
+ };
+
/**
* PlaybackFragment listener
*/
@@ -132,340 +123,6 @@
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 mSubtitle;
- private final TextView mCurrentTime;
- private final TextView mMaxTime;
- private final TextView mTimeSeparator;
- private final ImageView mActiveIcon;
-
- private final ImageViewBinder<MediaItemMetadata.ArtworkRef> mThumbnailBinder;
-
- 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.queue_list_item_title);
- mSubtitle = itemView.findViewById(R.id.queue_list_item_subtitle);
- 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);
-
- Size maxArtSize = MediaAppConfig.getMediaItemsBitmapMaxSize(itemView.getContext());
- mThumbnailBinder = new ImageViewBinder<>(maxArtSize, mThumbnail);
- }
-
- void bind(MediaItemMetadata item) {
- mView.setOnClickListener(v -> onQueueItemClicked(item));
-
- ViewUtils.setVisible(mThumbnailContainer, mShowThumbnailForQueueItem);
- if (mShowThumbnailForQueueItem) {
- Context context = mView.getContext();
- mThumbnailBinder.setImage(context, item != null ? item.getArtworkKey() : null);
- }
-
- 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);
-
- mView.setSelected(active);
-
- boolean shouldShowIcon = mShowIconForActiveQueueItem && active;
- ViewUtils.setVisible(mActiveIcon, shouldShowIcon);
-
- if (mShowSubtitleForQueueItem) {
- mSubtitle.setText(item.getSubtitle());
- }
- }
-
- void onViewAttachedToWindow() {
- if (mShowThumbnailForQueueItem) {
- Context context = mView.getContext();
- mThumbnailBinder.maybeRestartLoading(context);
- }
- }
-
- void onViewDetachedFromWindow() {
- if (mShowThumbnailForQueueItem) {
- Context context = mView.getContext();
- mThumbnailBinder.maybeCancelLoading(context);
- }
- }
- }
-
-
- private class QueueItemsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
- implements ContentLimiting {
-
- private static final int CLAMPED_MESSAGE_VIEW_TYPE = -1;
- private static final int QUEUE_ITEM_VIEW_TYPE = 0;
-
- private UxrPivotFilter mUxrPivotFilter;
- private List<MediaItemMetadata> mQueueItems = Collections.emptyList();
- private String mCurrentTimeText = "";
- private String mMaxTimeText = "";
- /** Index in {@link #mQueueItems}. */
- private Integer mActiveItemIndex;
- private boolean mTimeVisible;
- private Integer mScrollingLimitedMessageResId;
-
- QueueItemsAdapter() {
- mUxrPivotFilter = UxrPivotFilter.PASS_THROUGH;
- }
-
- @Override
- public void setMaxItems(int maxItems) {
- if (maxItems >= 0) {
- mUxrPivotFilter = new UxrPivotFilterImpl(this, maxItems);
- } else {
- mUxrPivotFilter = UxrPivotFilter.PASS_THROUGH;
- }
- applyFilterToQueue();
- }
-
- @Override
- public void setScrollingLimitedMessageResId(int resId) {
- if (mScrollingLimitedMessageResId == null || mScrollingLimitedMessageResId != resId) {
- mScrollingLimitedMessageResId = resId;
- mUxrPivotFilter.invalidateMessagePositions();
- }
- }
-
- @Override
- public int getConfigurationId() {
- return R.id.playback_fragment_now_playing_list_uxr_config;
- }
-
- void setItems(@Nullable List<MediaItemMetadata> items) {
- List<MediaItemMetadata> newQueueItems =
- new ArrayList<>(items != null ? items : Collections.emptyList());
- if (newQueueItems.equals(mQueueItems)) {
- return;
- }
- mQueueItems = newQueueItems;
- updateActiveItem(/* listIsNew */ true);
- }
-
- private int getActiveItemIndex() {
- return mActiveItemIndex != null ? mActiveItemIndex : 0;
- }
-
- private int getQueueSize() {
- return (mQueueItems != null) ? mQueueItems.size() : 0;
- }
-
-
- /**
- * Returns the position of the active item if there is one, otherwise returns
- * @link UxrPivotFilter#INVALID_POSITION}.
- */
- private int getActiveItemPosition() {
- if (mActiveItemIndex == null) {
- return UxrPivotFilter.INVALID_POSITION;
- }
- return mUxrPivotFilter.indexToPosition(mActiveItemIndex);
- }
-
- private void invalidateActiveItemPosition() {
- int position = getActiveItemPosition();
- if (position != UxrPivotFilterImpl.INVALID_POSITION) {
- notifyItemChanged(position);
- }
- }
-
- private void scrollToActiveItemPosition() {
- int position = getActiveItemPosition();
- if (position != UxrPivotFilterImpl.INVALID_POSITION) {
- mQueue.scrollToPosition(position);
- }
- }
-
- private void applyFilterToQueue() {
- mUxrPivotFilter.recompute(getQueueSize(), getActiveItemIndex());
- notifyDataSetChanged();
- }
-
- // Updates mActiveItemPos, then scrolls the queue to mActiveItemPos.
- // It should be called when the active item (mActiveQueueItemId) changed or
- // the queue items (mQueueItems) changed.
- void updateActiveItem(boolean listIsNew) {
- if (mQueueItems == null || mActiveQueueItemId == null) {
- mActiveItemIndex = null;
- applyFilterToQueue();
- return;
- }
- Integer activeItemPos = null;
- for (int i = 0; i < mQueueItems.size(); i++) {
- if (Objects.equals(mQueueItems.get(i).getQueueId(), mActiveQueueItemId)) {
- activeItemPos = i;
- break;
- }
- }
-
- // Invalidate the previous active item so it gets redrawn as a normal one.
- invalidateActiveItemPosition();
-
- mActiveItemIndex = activeItemPos;
- if (listIsNew) {
- applyFilterToQueue();
- } else {
- mUxrPivotFilter.updatePivotIndex(getActiveItemIndex());
- }
-
- scrollToActiveItemPosition();
- invalidateActiveItemPosition();
- }
-
- void setCurrentTime(String currentTime) {
- if (!mCurrentTimeText.equals(currentTime)) {
- mCurrentTimeText = currentTime;
- invalidateActiveItemPosition();
- }
- }
-
- void setMaxTime(String maxTime) {
- if (!mMaxTimeText.equals(maxTime)) {
- mMaxTimeText = maxTime;
- invalidateActiveItemPosition();
- }
- }
-
- void setTimeVisible(boolean visible) {
- if (mTimeVisible != visible) {
- mTimeVisible = visible;
- invalidateActiveItemPosition();
- }
- }
-
- String getCurrentTime() {
- return mCurrentTimeText;
- }
-
- String getMaxTime() {
- return mMaxTimeText;
- }
-
- boolean getTimeVisible() {
- return mTimeVisible;
- }
-
- @Override
- public final int getItemViewType(int position) {
- if (mUxrPivotFilter.positionToIndex(position) == UxrPivotFilterImpl.INVALID_INDEX) {
- return CLAMPED_MESSAGE_VIEW_TYPE;
- } else {
- return QUEUE_ITEM_VIEW_TYPE;
- }
- }
-
- @Override
- public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- if (viewType == CLAMPED_MESSAGE_VIEW_TYPE) {
- return ScrollingLimitedViewHolder.create(parent);
- }
- LayoutInflater inflater = LayoutInflater.from(parent.getContext());
- return new QueueViewHolder(inflater.inflate(R.layout.queue_list_item, parent, false));
- }
-
- @Override
- public void onBindViewHolder(RecyclerView.ViewHolder vh, int position) {
- if (vh instanceof QueueViewHolder) {
- int index = mUxrPivotFilter.positionToIndex(position);
- if (index != UxrPivotFilterImpl.INVALID_INDEX) {
- int size = mQueueItems.size();
- if (0 <= index && index < size) {
- QueueViewHolder holder = (QueueViewHolder) vh;
- holder.bind(mQueueItems.get(index));
- } else {
- Log.e(TAG, "onBindViewHolder pos: " + position + " gave index: " + index +
- " out of bounds size: " + size + " " + mUxrPivotFilter.toString());
- }
- } else {
- Log.e(TAG, "onBindViewHolder invalid position " + position + " " +
- mUxrPivotFilter.toString());
- }
- } else if (vh instanceof ScrollingLimitedViewHolder) {
- ScrollingLimitedViewHolder holder = (ScrollingLimitedViewHolder) vh;
- holder.bind(mScrollingLimitedMessageResId);
- } else {
- throw new IllegalArgumentException("unknown holder class " + vh.getClass());
- }
- }
-
- @Override
- public void onViewAttachedToWindow(@NonNull RecyclerView.ViewHolder vh) {
- super.onViewAttachedToWindow(vh);
- if (vh instanceof QueueViewHolder) {
- QueueViewHolder holder = (QueueViewHolder) vh;
- holder.onViewAttachedToWindow();
- }
- }
-
- @Override
- public void onViewDetachedFromWindow(@NonNull RecyclerView.ViewHolder vh) {
- super.onViewDetachedFromWindow(vh);
- if (vh instanceof QueueViewHolder) {
- QueueViewHolder holder = (QueueViewHolder) vh;
- holder.onViewDetachedFromWindow();
- }
- }
-
- @Override
- public int getItemCount() {
- return mUxrPivotFilter.getFilteredCount();
- }
-
- @Override
- public long getItemId(int position) {
- int index = mUxrPivotFilter.positionToIndex(position);
- if (index != UxrPivotFilterImpl.INVALID_INDEX) {
- return mQueueItems.get(position).getQueueId();
- } else {
- return RecyclerView.NO_ID;
- }
- }
- }
-
- private static class QueueTopItemDecoration extends RecyclerView.ItemDecoration {
- int mHeight;
- int mDecorationPosition;
-
- QueueTopItemDecoration(int height, int decorationPosition) {
- mHeight = height;
- mDecorationPosition = decorationPosition;
- }
-
- @Override
- 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) {
@@ -477,20 +134,33 @@
mPlaybackViewModel = PlaybackViewModel.get(getActivity().getApplication(),
MEDIA_SOURCE_MODE_PLAYBACK);
+ Resources res = getResources();
mAlbumBackground = view.findViewById(R.id.playback_background);
- mQueue = view.findViewById(R.id.queue_list);
+ mPlaybackQueueFragment = new PlaybackQueueFragment();
+ mPlaybackQueueFragment.setCallback(mPlaybackQueueCallback);
+
+ getChildFragmentManager().beginTransaction()
+ .add(R.id.queue_fragment_container, mPlaybackQueueFragment)
+ .commit();
+
mSeekBarContainer = view.findViewById(R.id.playback_seek_bar_container);
mSeekBar = view.findViewById(R.id.playback_seek_bar);
DirectManipulationHelper.setSupportsRotateDirectly(mSeekBar, true);
GuidelinesUpdater updater = new GuidelinesUpdater(view);
ToolbarController toolbarController = CarUi.installBaseLayoutAround(view, updater, true);
- mAppBarController = new AppBarController(view.getContext(), toolbarController);
+ mAppBarController = new AppBarController(view.getContext(), toolbarController,
+ R.xml.menuitems_playback,
+ res.getBoolean(R.bool.use_media_source_logo_for_app_selector_in_playback_view));
mAppBarController.setTitle(R.string.fragment_playback_title);
mAppBarController.setBackgroundShown(false);
mAppBarController.setNavButtonMode(NavButtonMode.DOWN);
+ mQueueMenuItem = mAppBarController.getMenuItem(R.id.menu_item_queue);
+ Preconditions.checkNotNull(mQueueMenuItem);
+ mQueueMenuItem.setOnClickListener((item) -> toggleQueueVisibility());
+
// Update toolbar's logo
MediaSourceViewModel mediaSourceViewModel = getMediaSourceViewModel();
mediaSourceViewModel.getPrimaryMediaSource().observe(this, mediaSource ->
@@ -507,18 +177,6 @@
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);
- mShowLinearProgressBar = getContext().getResources().getBoolean(
- R.bool.show_linear_progress_bar);
- mShowSubtitleForQueueItem = getContext().getResources().getBoolean(
- R.bool.show_subtitle_for_queue_list_item);
-
if (mSeekBar != null) {
if (mShowLinearProgressBar) {
boolean useMediaSourceColor = res.getBoolean(
@@ -542,8 +200,6 @@
mViewModel = ViewModelProviders.of(requireActivity()).get(MediaActivity.ViewModel.class);
- mPlaybackViewModel.getPlaybackController().observe(getViewLifecycleOwner(),
- controller -> mController = controller);
initPlaybackControls(view.findViewById(R.id.playback_controls));
initMetadataController(view);
initQueue();
@@ -571,11 +227,6 @@
mPlaybackViewModel.getMetadata().observe(getViewLifecycleOwner(),
item -> mAlbumArtBinder.setImage(PlaybackFragment.this.getContext(),
item != null ? item.getArtworkKey() : null));
-
- mUxrContentLimiter = new LifeCycleObserverUxrContentLimiter(
- new UxrContentLimiterImpl(getContext(), R.xml.uxr_config));
- mUxrContentLimiter.setAdapter(mQueueAdapter);
- getLifecycle().addObserver(mUxrContentLimiter);
}
@Override
@@ -626,60 +277,18 @@
mFadeDuration = getResources().getInteger(
R.integer.fragment_playback_queue_fade_duration_ms);
- int decorationHeight = getResources().getDimensionPixelSize(
- R.dimen.playback_queue_list_padding_top);
- // TODO (b/206038962): addItemDecoration is not supported anymore. Find another way to
- // support this.
- // 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();
-
- mPlaybackViewModel.getPlaybackStateWrapper().observe(getViewLifecycleOwner(),
- state -> {
- Long itemId = (state != null) ? state.getActiveQueueItemId() : null;
- if (!Objects.equals(mActiveQueueItemId, itemId)) {
- mActiveQueueItemId = itemId;
- mQueueAdapter.updateActiveItem(/* listIsNew */ false);
- }
- });
- mQueue.setAdapter(mQueueAdapter);
-
- // Disable item changed animation.
- mItemAnimator = new DefaultItemAnimator();
- mItemAnimator.setSupportsChangeAnimations(false);
- mQueue.setItemAnimator(mItemAnimator);
-
// Make sure the AppBar menu reflects the initial state of playback fragment.
updateAppBarMenu(mHasQueue);
if (mQueueMenuItem != null) {
mQueueMenuItem.setActivated(mQueueIsVisible);
}
- mPlaybackViewModel.getQueue().observe(this, this::setQueue);
-
mPlaybackViewModel.hasQueue().observe(getViewLifecycleOwner(),
hasQueue -> {
boolean enableQueue = (hasQueue != null) && hasQueue;
boolean isQueueVisible = enableQueue && mViewModel.getQueueVisible();
setQueueState(enableQueue, isQueueVisible);
});
-
- mPlaybackViewModel.getProgress().observe(
- getViewLifecycleOwner(),
- playbackProgress ->
- {
- mQueueAdapter.setCurrentTime(playbackProgress.getCurrentTimeText().toString());
- mQueueAdapter.setMaxTime(playbackProgress.getMaxTimeText().toString());
- mQueueAdapter.setTimeVisible(playbackProgress.hasTime());
- });
- }
-
- private void setQueue(List<MediaItemMetadata> queueItems) {
- mQueueAdapter.setItems(queueItems);
}
private void initMetadataController(View view) {
@@ -714,18 +323,7 @@
}
private void updateAppBarMenu(boolean hasQueue) {
- if (hasQueue && mQueueMenuItem == null) {
- List<MenuItem> menuItems = MenuItemXmlParserUtil.readMenuItemList(getContext(),
- R.xml.menuitems_playback);
- menuItems.forEach((menuItem -> {
- if (menuItem.getId() == R.id.menu_item_queue) {
- mQueueMenuItem = menuItem;
- mQueueMenuItem.setOnClickListener((item) -> toggleQueueVisibility());
- }
- }));
- }
- mAppBarController.setMenuItems(
- hasQueue ? Collections.singletonList(mQueueMenuItem) : Collections.emptyList());
+ mQueueMenuItem.setVisible(hasQueue);
}
private void setQueueState(boolean hasQueue, boolean visible) {
@@ -751,17 +349,6 @@
}
}
- private void onQueueItemClicked(MediaItemMetadata item) {
- 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();
- }
- }
-
/**
* Collapses the playback controls.
*/
diff --git a/src/com/android/car/media/PlaybackQueueFragment.java b/src/com/android/car/media/PlaybackQueueFragment.java
new file mode 100644
index 0000000..041065b
--- /dev/null
+++ b/src/com/android/car/media/PlaybackQueueFragment.java
@@ -0,0 +1,531 @@
+/*
+ * 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 static android.car.media.CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.util.Log;
+import android.util.Size;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.DefaultItemAnimator;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.apps.common.imaging.ImageViewBinder;
+import com.android.car.apps.common.util.ViewUtils;
+import com.android.car.media.common.MediaItemMetadata;
+import com.android.car.media.common.playback.PlaybackViewModel;
+import com.android.car.ui.recyclerview.CarUiRecyclerView;
+import com.android.car.ui.recyclerview.ContentLimiting;
+import com.android.car.ui.recyclerview.ScrollingLimitedViewHolder;
+import com.android.car.uxr.LifeCycleObserverUxrContentLimiter;
+import com.android.car.uxr.UxrContentLimiterImpl;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+
+/**
+ * A {@link Fragment} that implements the playback queue experience. 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 PlaybackQueueFragment extends Fragment {
+
+ private static final String TAG = "PlaybackQueueFragment";
+
+ private LifeCycleObserverUxrContentLimiter mUxrContentLimiter;
+ private PlaybackViewModel mPlaybackViewModel;
+ private QueueItemsAdapter mQueueAdapter;
+ private CarUiRecyclerView mQueue;
+ private PlaybackQueueCallback mPlaybackQueueCallback;
+
+ private DefaultItemAnimator mItemAnimator;
+
+ private PlaybackViewModel.PlaybackController mController;
+ private Long mActiveQueueItemId;
+
+ private boolean mShowTimeForActiveQueueItem;
+ private boolean mShowIconForActiveQueueItem;
+ private boolean mShowThumbnailForQueueItem;
+ private boolean mShowSubtitleForQueueItem;
+
+ /**
+ * The callbacks used to communicate the user interactions to the queue fragment listeners.
+ */
+ public interface PlaybackQueueCallback {
+
+ /**
+ * Will be called when a queue item is selected by the user.
+ **/
+ void onQueueItemClicked(MediaItemMetadata item);
+ }
+
+ /**
+ * The view holder for the queue items.
+ */
+ 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 mSubtitle;
+ private final TextView mCurrentTime;
+ private final TextView mMaxTime;
+ private final TextView mTimeSeparator;
+ private final ImageView mActiveIcon;
+
+ private final ImageViewBinder<MediaItemMetadata.ArtworkRef> mThumbnailBinder;
+
+ 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.queue_list_item_title);
+ mSubtitle = itemView.findViewById(R.id.queue_list_item_subtitle);
+ 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);
+
+ Size maxArtSize = MediaAppConfig.getMediaItemsBitmapMaxSize(itemView.getContext());
+ mThumbnailBinder = new ImageViewBinder<>(maxArtSize, mThumbnail);
+ }
+
+ void bind(MediaItemMetadata item) {
+ mView.setOnClickListener(v -> onQueueItemClicked(item));
+
+ ViewUtils.setVisible(mThumbnailContainer, mShowThumbnailForQueueItem);
+ if (mShowThumbnailForQueueItem) {
+ Context context = mView.getContext();
+ mThumbnailBinder.setImage(context, item != null ? item.getArtworkKey() : null);
+ }
+
+ 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);
+
+ mView.setSelected(active);
+
+ boolean shouldShowIcon = mShowIconForActiveQueueItem && active;
+ ViewUtils.setVisible(mActiveIcon, shouldShowIcon);
+
+ if (mShowSubtitleForQueueItem) {
+ mSubtitle.setText(item.getSubtitle());
+ }
+ }
+
+ void onViewAttachedToWindow() {
+ if (mShowThumbnailForQueueItem) {
+ Context context = mView.getContext();
+ mThumbnailBinder.maybeRestartLoading(context);
+ }
+ }
+
+ void onViewDetachedFromWindow() {
+ if (mShowThumbnailForQueueItem) {
+ Context context = mView.getContext();
+ mThumbnailBinder.maybeCancelLoading(context);
+ }
+ }
+ }
+
+
+ private class QueueItemsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
+ implements ContentLimiting {
+
+ private static final int CLAMPED_MESSAGE_VIEW_TYPE = -1;
+ private static final int QUEUE_ITEM_VIEW_TYPE = 0;
+
+ private UxrPivotFilter mUxrPivotFilter;
+ private List<MediaItemMetadata> mQueueItems = Collections.emptyList();
+ private String mCurrentTimeText = "";
+ private String mMaxTimeText = "";
+ /**
+ * Index in {@link #mQueueItems}.
+ */
+ private Integer mActiveItemIndex;
+ private boolean mTimeVisible;
+ private Integer mScrollingLimitedMessageResId;
+
+ QueueItemsAdapter() {
+ mUxrPivotFilter = UxrPivotFilter.PASS_THROUGH;
+ }
+
+ @Override
+ public void setMaxItems(int maxItems) {
+ if (maxItems >= 0) {
+ mUxrPivotFilter = new UxrPivotFilterImpl(this, maxItems);
+ } else {
+ mUxrPivotFilter = UxrPivotFilter.PASS_THROUGH;
+ }
+ applyFilterToQueue();
+ }
+
+ @Override
+ public void setScrollingLimitedMessageResId(int resId) {
+ if (mScrollingLimitedMessageResId == null || mScrollingLimitedMessageResId != resId) {
+ mScrollingLimitedMessageResId = resId;
+ mUxrPivotFilter.invalidateMessagePositions();
+ }
+ }
+
+ @Override
+ public int getConfigurationId() {
+ return R.id.playback_fragment_now_playing_list_uxr_config;
+ }
+
+ void setItems(@Nullable List<MediaItemMetadata> items) {
+ List<MediaItemMetadata> newQueueItems =
+ new ArrayList<>(items != null ? items : Collections.emptyList());
+ if (newQueueItems.equals(mQueueItems)) {
+ return;
+ }
+ mQueueItems = newQueueItems;
+ updateActiveItem(/* listIsNew */ true);
+ }
+
+ private int getActiveItemIndex() {
+ return mActiveItemIndex != null ? mActiveItemIndex : 0;
+ }
+
+ private int getQueueSize() {
+ return (mQueueItems != null) ? mQueueItems.size() : 0;
+ }
+
+
+ /**
+ * Returns the position of the active item if there is one, otherwise returns
+ *
+ * @link UxrPivotFilter#INVALID_POSITION}.
+ */
+ private int getActiveItemPosition() {
+ if (mActiveItemIndex == null) {
+ return UxrPivotFilter.INVALID_POSITION;
+ }
+ return mUxrPivotFilter.indexToPosition(mActiveItemIndex);
+ }
+
+ private void invalidateActiveItemPosition() {
+ int position = getActiveItemPosition();
+ if (position != UxrPivotFilterImpl.INVALID_POSITION) {
+ notifyItemChanged(position);
+ }
+ }
+
+ private void scrollToActiveItemPosition() {
+ int position = getActiveItemPosition();
+ if (position != UxrPivotFilterImpl.INVALID_POSITION) {
+ mQueue.scrollToPosition(position);
+ }
+ }
+
+ private void applyFilterToQueue() {
+ mUxrPivotFilter.recompute(getQueueSize(), getActiveItemIndex());
+ notifyDataSetChanged();
+ }
+
+ // Updates mActiveItemPos, then scrolls the queue to mActiveItemPos.
+ // It should be called when the active item (mActiveQueueItemId) changed or
+ // the queue items (mQueueItems) changed.
+ void updateActiveItem(boolean listIsNew) {
+ if (mQueueItems == null || mActiveQueueItemId == null) {
+ mActiveItemIndex = null;
+ applyFilterToQueue();
+ return;
+ }
+ Integer activeItemPos = null;
+ for (int i = 0; i < mQueueItems.size(); i++) {
+ if (Objects.equals(mQueueItems.get(i).getQueueId(), mActiveQueueItemId)) {
+ activeItemPos = i;
+ break;
+ }
+ }
+
+ // Invalidate the previous active item so it gets redrawn as a normal one.
+ invalidateActiveItemPosition();
+
+ mActiveItemIndex = activeItemPos;
+ if (listIsNew) {
+ applyFilterToQueue();
+ } else {
+ mUxrPivotFilter.updatePivotIndex(getActiveItemIndex());
+ }
+
+ scrollToActiveItemPosition();
+ invalidateActiveItemPosition();
+ }
+
+ void setCurrentTime(String currentTime) {
+ if (!mCurrentTimeText.equals(currentTime)) {
+ mCurrentTimeText = currentTime;
+ invalidateActiveItemPosition();
+ }
+ }
+
+ void setMaxTime(String maxTime) {
+ if (!mMaxTimeText.equals(maxTime)) {
+ mMaxTimeText = maxTime;
+ invalidateActiveItemPosition();
+ }
+ }
+
+ void setTimeVisible(boolean visible) {
+ if (mTimeVisible != visible) {
+ mTimeVisible = visible;
+ invalidateActiveItemPosition();
+ }
+ }
+
+ String getCurrentTime() {
+ return mCurrentTimeText;
+ }
+
+ String getMaxTime() {
+ return mMaxTimeText;
+ }
+
+ boolean getTimeVisible() {
+ return mTimeVisible;
+ }
+
+ @Override
+ public final int getItemViewType(int position) {
+ if (mUxrPivotFilter.positionToIndex(position) == UxrPivotFilterImpl.INVALID_INDEX) {
+ return CLAMPED_MESSAGE_VIEW_TYPE;
+ } else {
+ return QUEUE_ITEM_VIEW_TYPE;
+ }
+ }
+
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ if (viewType == CLAMPED_MESSAGE_VIEW_TYPE) {
+ return ScrollingLimitedViewHolder.create(parent);
+ }
+ LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+ return new QueueViewHolder(inflater.inflate(R.layout.queue_list_item, parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder vh, int position) {
+ if (vh instanceof QueueViewHolder) {
+ int index = mUxrPivotFilter.positionToIndex(position);
+ if (index != UxrPivotFilterImpl.INVALID_INDEX) {
+ int size = mQueueItems.size();
+ if (0 <= index && index < size) {
+ QueueViewHolder holder = (QueueViewHolder) vh;
+ holder.bind(mQueueItems.get(index));
+ } else {
+ Log.e(TAG, "onBindViewHolder pos: " + position + " gave index: "
+ + index + " out of bounds size: " + size + " "
+ + mUxrPivotFilter.toString());
+ }
+ } else {
+ Log.e(TAG, "onBindViewHolder invalid position " + position + " "
+ + mUxrPivotFilter.toString());
+ }
+ } else if (vh instanceof ScrollingLimitedViewHolder) {
+ ScrollingLimitedViewHolder holder = (ScrollingLimitedViewHolder) vh;
+ holder.bind(mScrollingLimitedMessageResId);
+ } else {
+ throw new IllegalArgumentException("unknown holder class " + vh.getClass());
+ }
+ }
+
+ @Override
+ public void onViewAttachedToWindow(@NonNull RecyclerView.ViewHolder vh) {
+ super.onViewAttachedToWindow(vh);
+ if (vh instanceof QueueViewHolder) {
+ QueueViewHolder holder = (QueueViewHolder) vh;
+ holder.onViewAttachedToWindow();
+ }
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(@NonNull RecyclerView.ViewHolder vh) {
+ super.onViewDetachedFromWindow(vh);
+ if (vh instanceof QueueViewHolder) {
+ QueueViewHolder holder = (QueueViewHolder) vh;
+ holder.onViewDetachedFromWindow();
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return mUxrPivotFilter.getFilteredCount();
+ }
+
+ @Override
+ public long getItemId(int position) {
+ int index = mUxrPivotFilter.positionToIndex(position);
+ if (index != UxrPivotFilterImpl.INVALID_INDEX) {
+ return mQueueItems.get(position).getQueueId();
+ } else {
+ return RecyclerView.NO_ID;
+ }
+ }
+ }
+
+ private static class QueueTopItemDecoration extends RecyclerView.ItemDecoration {
+ int mHeight;
+ int mDecorationPosition;
+
+ QueueTopItemDecoration(int height, int decorationPosition) {
+ mHeight = height;
+ mDecorationPosition = decorationPosition;
+ }
+
+ @Override
+ 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) {
+ return inflater.inflate(R.layout.fragment_playback_queue, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ mPlaybackViewModel = PlaybackViewModel.get(getActivity().getApplication(),
+ MEDIA_SOURCE_MODE_PLAYBACK);
+
+ Resources res = getResources();
+ mQueue = view.findViewById(R.id.queue_list);
+
+ 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);
+ mShowSubtitleForQueueItem = getContext().getResources().getBoolean(
+ R.bool.show_subtitle_for_queue_list_item);
+
+ mPlaybackViewModel.getPlaybackController().observe(getViewLifecycleOwner(),
+ controller -> mController = controller);
+ initQueue();
+
+ mUxrContentLimiter = new LifeCycleObserverUxrContentLimiter(
+ new UxrContentLimiterImpl(getContext(), R.xml.uxr_config));
+ mUxrContentLimiter.setAdapter(mQueueAdapter);
+ getLifecycle().addObserver(mUxrContentLimiter);
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ }
+
+
+ public void setCallback(PlaybackQueueCallback callback) {
+ mPlaybackQueueCallback = callback;
+ }
+
+ private void initQueue() {
+
+ int decorationHeight = getResources().getDimensionPixelSize(
+ R.dimen.playback_queue_list_padding_top);
+ // TODO (b/206038962): addItemDecoration is not supported anymore. Find another way to
+ // support this.
+ // 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();
+
+ mPlaybackViewModel.getPlaybackStateWrapper().observe(getViewLifecycleOwner(),
+ state -> {
+ Long itemId = (state != null) ? state.getActiveQueueItemId() : null;
+ if (!Objects.equals(mActiveQueueItemId, itemId)) {
+ mActiveQueueItemId = itemId;
+ mQueueAdapter.updateActiveItem(/* listIsNew */ false);
+ }
+ });
+ mQueue.setAdapter(mQueueAdapter);
+
+ // Disable item changed animation.
+ mItemAnimator = new DefaultItemAnimator();
+ mItemAnimator.setSupportsChangeAnimations(false);
+ mQueue.setItemAnimator(mItemAnimator);
+ mPlaybackViewModel.getQueue().observe(this, this::setQueue);
+
+ mPlaybackViewModel.getProgress().observe(
+ getViewLifecycleOwner(),
+ playbackProgress -> {
+ mQueueAdapter.setCurrentTime(playbackProgress.getCurrentTimeText().toString());
+ mQueueAdapter.setMaxTime(playbackProgress.getMaxTimeText().toString());
+ mQueueAdapter.setTimeVisible(playbackProgress.hasTime());
+ });
+ }
+
+ void setQueue(List<MediaItemMetadata> queueItems) {
+ mQueueAdapter.setItems(queueItems);
+ }
+
+ private void onQueueItemClicked(MediaItemMetadata item) {
+ if (mController != null) {
+ mController.skipToQueueItem(item.getQueueId());
+ }
+ if (mPlaybackQueueCallback != null) {
+ mPlaybackQueueCallback.onQueueItemClicked(item);
+ }
+ }
+}
diff --git a/src/com/android/car/media/ViewControllerBase.java b/src/com/android/car/media/ViewControllerBase.java
index 4b4ba18..0b66816 100644
--- a/src/com/android/car/media/ViewControllerBase.java
+++ b/src/com/android/car/media/ViewControllerBase.java
@@ -85,7 +85,9 @@
updater.addListener(this);
ToolbarController toolbar = CarUi.installBaseLayoutAround(mContent, updater, true);
- mAppBarController = new AppBarController(activity, toolbar);
+ mAppBarController = new AppBarController(activity, toolbar, R.xml.menuitems_browse,
+ res.getBoolean(R.bool.use_media_source_logo_for_app_selector));
+ mAppBarController.checkBrowseMenus();
mAppBarController.setSearchSupported(false);
mAppBarController.setHasEqualizer(false);
diff --git a/src/com/android/car/media/browse/BrowseAdapter.java b/src/com/android/car/media/browse/BrowseAdapter.java
index 42fbff8..89e1c49 100644
--- a/src/com/android/car/media/browse/BrowseAdapter.java
+++ b/src/com/android/car/media/browse/BrowseAdapter.java
@@ -31,11 +31,13 @@
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
+import com.android.car.media.common.CustomBrowseAction;
import com.android.car.media.common.MediaItemMetadata;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
@@ -62,14 +64,25 @@
void onListChanged(List<BrowseViewData> previousList, List<BrowseViewData> currentList);
}
+ /**
+ * Type of update for the media item.
+ * Progress will update the progress UI
+ * Browse actions will update the browse actions ui
+ */
+ public enum MediaItemUpdateType{
+ PROGRESS,
+ BROWSE_ACTIONS
+ }
+
@NonNull
private final Context mContext;
@NonNull
- private List<Observer> mObservers = new ArrayList<>();
+ private final List<Observer> mObservers = new ArrayList<>();
@Nullable
private CharSequence mTitle;
@Nullable
private MediaItemMetadata mParentMediaItem;
+ private Map<String, CustomBrowseAction> mGlobalCustomBrowseActions;
private BrowseItemViewType mRootBrowsableViewType = BrowseItemViewType.LIST_ITEM;
private BrowseItemViewType mRootPlayableViewType = BrowseItemViewType.LIST_ITEM;
@@ -79,29 +92,35 @@
private static final DiffUtil.ItemCallback<BrowseViewData> DIFF_CALLBACK =
new DiffUtil.ItemCallback<BrowseViewData>() {
@Override
- public boolean areItemsTheSame(@NonNull BrowseViewData oldItem,
- @NonNull BrowseViewData newItem) {
+ public boolean areItemsTheSame(
+ @NonNull BrowseViewData oldItem, @NonNull BrowseViewData newItem) {
return Objects.equals(
- oldItem.mMediaItem != null ? oldItem.mMediaItem.getId() : null,
- newItem.mMediaItem != null ? newItem.mMediaItem.getId() : null)
+ oldItem.mMediaItem != null ? oldItem.mMediaItem.getId() : null,
+ newItem.mMediaItem != null ? newItem.mMediaItem.getId() : null)
&& Objects.equals(oldItem.mText, newItem.mText);
}
@Override
- public boolean areContentsTheSame(@NonNull BrowseViewData oldItem,
- @NonNull BrowseViewData newItem) {
+ public boolean areContentsTheSame(
+ @NonNull BrowseViewData oldItem, @NonNull BrowseViewData newItem) {
return Objects.equals(oldItem, newItem);
}
@Nullable
@Override
- public Object getChangePayload(@NonNull BrowseViewData oldItem,
- @NonNull BrowseViewData newItem) {
- if (oldItem == newItem || Objects.equals(oldItem.mUpdatedMediaItem,
- newItem.mUpdatedMediaItem)) {
+ public Object getChangePayload(
+ @NonNull BrowseViewData oldItem, @NonNull BrowseViewData newItem) {
+ if (oldItem == newItem || Objects.equals(oldItem, newItem)) {
return super.getChangePayload(oldItem, newItem);
} else {
- return newItem.mUpdatedMediaItem;
+ if (!newItem.mCustomBrowseActions.equals(oldItem.mCustomBrowseActions)) {
+ return MediaItemUpdateType.BROWSE_ACTIONS;
+ }
+ if (Math.abs(newItem.mMediaItem.getProgress()
+ - oldItem.mMediaItem.getProgress()) > .005) {
+ return MediaItemUpdateType.PROGRESS;
+ }
+ return null;
}
}
};
@@ -109,27 +128,69 @@
/**
* An {@link BrowseAdapter} observer.
*/
- public static abstract class Observer {
+ public abstract static class Observer {
/**
* Callback invoked when a user clicks on a playable item.
*/
protected void onPlayableItemClicked(@NonNull MediaItemMetadata item) {
}
-
/**
* Callback invoked when a user clicks on a browsable item.
*/
protected void onBrowsableItemClicked(@NonNull MediaItemMetadata item) {
}
+ /** Callback invoked when a user clicks on a browse custom action */
+ protected void onBrowseCustomActionClicked(
+ @NonNull CustomBrowseAction customBrowseAction, String mediaID) {}
+
+ /** Callback invoked when a user clicks on the browse custom action overflow */
+ protected void onBrowseCustomActionOverflowClicked(
+ @NonNull List<CustomBrowseAction> overflowActions, String mediaID) {}
+
/**
- * Callback invoked when the user clicks on the title of the queue.
+ * Callback invoked when the user clicks on a header type item.
*/
- protected void onTitleClicked() {
+ protected void onHeaderItemClicked() {
}
}
+ private final BrowseViewData.BrowseViewDataCallback mCallback =
+ new BrowseViewData.BrowseViewDataCallback() {
+ @Override
+ public void onMediaItemClicked(BrowseViewData item) {
+ BrowseAdapter.this.notify(observer -> {
+ if (item.mViewType == BrowseItemViewType.HEADER) {
+ observer.onHeaderItemClicked();
+ } else {
+ if (item.mMediaItem.isPlayable()) {
+ observer.onPlayableItemClicked(item.mMediaItem);
+ }
+ if (item.mMediaItem.isBrowsable()) {
+ observer.onBrowsableItemClicked(item.mMediaItem);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onBrowseActionClick(
+ CustomBrowseAction action, BrowseViewData browseViewData) {
+ BrowseAdapter.this.notify(observer ->
+ observer.onBrowseCustomActionClicked(
+ action, browseViewData.mMediaItem.getId()));
+ }
+
+ @Override
+ public void onOverflowClicked(
+ List<CustomBrowseAction> items, BrowseViewData browseViewData) {
+ BrowseAdapter.this.notify(
+ observer -> observer.onBrowseCustomActionOverflowClicked(
+ items, browseViewData.mMediaItem.getId()));
+ }
+ };
+
/**
* Creates a {@link BrowseAdapter} that displays the children of the given media tree node.
*/
@@ -167,6 +228,11 @@
mRootPlayableViewType = fromMediaHint(hintValue);
}
+ /** Sets the list of custom browse actions */
+ public void setGlobalCustomActions(Map<String, CustomBrowseAction> customBrowseActions) {
+ this.mGlobalCustomBrowseActions = customBrowseActions;
+ }
+
public int getSpanSize(int position, int maxSpanSize) {
BrowseItemViewType viewType = getItem(position).mViewType;
return viewType.getSpanSize(maxSpanSize);
@@ -196,14 +262,24 @@
@Override
public void onBindViewHolder(@NonNull BrowseViewHolder holder, int position,
@NonNull List<Object> payloads) {
- //We are only checking for MediaMetaData for now, since this is the only payload we are
- // setting in getChangePayload
- if (payloads.isEmpty() || !(payloads.get(0) instanceof MediaItemMetadata)) {
+ if (payloads.isEmpty()) {
BrowseViewData viewData = getItem(position);
holder.bind(mContext, viewData);
- } else {
- MediaItemMetadata mediaMetadata = (MediaItemMetadata) payloads.get(0);
- holder.update(mediaMetadata);
+ }
+ // These can be merged into a list, a few notify change calls can happen at once,
+ // and the payloads will be merged into a single payload list
+ for (Object payload : payloads) {
+ if (payload instanceof BrowseViewData) {
+ BrowseViewData browseViewData = (BrowseViewData) payload;
+ holder.update(browseViewData, null);
+ } else if (payload instanceof MediaItemUpdateType) {
+ BrowseViewData viewData = getItem(position);
+ MediaItemUpdateType updateType = (MediaItemUpdateType) payload;
+ holder.update(viewData, updateType);
+ } else {
+ BrowseViewData viewData = getItem(position);
+ holder.bind(mContext, viewData);
+ }
}
}
@@ -252,22 +328,20 @@
private class ItemsBuilder {
private List<BrowseViewData> result = new ArrayList<>();
- 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, listener));
+ void addItem(
+ MediaItemMetadata item,
+ BrowseItemViewType viewType,
+ @NonNull List<CustomBrowseAction> customActions) {
+ result.add(new BrowseViewData(item, viewType, customActions, mCallback));
}
- void addTitle(CharSequence title, Consumer<Observer> notification) {
+ void addTitle(CharSequence title) {
if (title == null) {
title = "";
}
- View.OnClickListener listener = notification != null ?
- view -> BrowseAdapter.this.notify(notification) :
- null;
- result.add(new BrowseViewData(title, BrowseItemViewType.HEADER, listener));
+ result.add(
+ new BrowseViewData(
+ title, BrowseItemViewType.HEADER, Collections.emptyList(), mCallback));
}
List<BrowseViewData> build() {
@@ -285,30 +359,35 @@
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Generating browse view from:");
for (MediaItemMetadata item : items) {
- Log.v(TAG, String.format("[%s%s] '%s' (%s)",
- item.isBrowsable() ? "B" : " ",
- item.isPlayable() ? "P" : " ",
- item.getTitle(),
- item.getId()));
+ Log.v(TAG,
+ String.format(
+ "[%s%s] '%s' (%s)",
+ item.isBrowsable() ? "B" : " ",
+ item.isPlayable() ? "P" : " ",
+ item.getTitle(),
+ item.getId()));
}
}
if (mTitle != null) {
- itemsBuilder.addTitle(mTitle, Observer::onTitleClicked);
+ itemsBuilder.addTitle(mTitle);
}
String currentTitleGrouping = null;
for (MediaItemMetadata item : items) {
+ List<CustomBrowseAction> customBrowseActions =
+ buildBrowseActions(mContext, item);
+
String titleGrouping = item.getTitleGrouping();
if (!Objects.equals(currentTitleGrouping, titleGrouping)) {
currentTitleGrouping = titleGrouping;
- itemsBuilder.addTitle(titleGrouping, null);
+ itemsBuilder.addTitle(titleGrouping);
}
if (item.isBrowsable()) {
- itemsBuilder.addItem(item, getBrowsableViewType(mParentMediaItem, item),
- observer -> observer.onBrowsableItemClicked(item));
+ itemsBuilder.addItem(
+ item, getBrowsableViewType(mParentMediaItem, item), customBrowseActions);
} else if (item.isPlayable()) {
- itemsBuilder.addItem(item, getPlayableViewType(mParentMediaItem, item),
- observer -> observer.onPlayableItemClicked(item));
+ itemsBuilder.addItem(
+ item, getPlayableViewType(mParentMediaItem, item), customBrowseActions);
}
}
@@ -387,8 +466,7 @@
} else {
return false;
}
- }
- )
+ })
.findFirst()
.orElse(null);
}
@@ -397,8 +475,6 @@
* <p>
* This should call a partial bind with the new metadata as the diff payload,
* meaning it will use bind with payload when view is visible or full bind when not.
- * the payload will then be used to only update the progress bar and not the
- * whole item's UI.
* Use {@link androidx.recyclerview.widget.RecyclerView.AdapterDataObservable} to
* listen to when there is a payload change called here.
*
@@ -414,12 +490,25 @@
* ways here where we can use a partial bind for performance.
* </p>
*/
- void updateItemMetaData(MediaItemMetadata mediaItemMetadata) {
+ void updateItemMetaData(MediaItemMetadata mediaItemMetadata, MediaItemUpdateType updateType) {
BrowseViewData browseViewData = getMediaByMetaData(mediaItemMetadata.getId());
if (browseViewData != null) {
+ if (updateType == MediaItemUpdateType.BROWSE_ACTIONS) {
+ List<CustomBrowseAction> newActions =
+ buildBrowseActions(mContext, mediaItemMetadata);
+ browseViewData.mCustomBrowseActions.clear();
+ browseViewData.mCustomBrowseActions.addAll(newActions);
+ }
int position = getCurrentList().indexOf(browseViewData);
- browseViewData.mUpdatedMediaItem = mediaItemMetadata;
- notifyItemChanged(position, mediaItemMetadata);
+ notifyItemChanged(position, updateType);
}
}
+
+ private List<CustomBrowseAction> buildBrowseActions(
+ Context context, MediaItemMetadata mediaItemMetadata) {
+ return BrowseAdapterUtils.buildBrowseCustomActions(
+ context,
+ mediaItemMetadata,
+ mGlobalCustomBrowseActions);
+ }
}
diff --git a/src/com/android/car/media/browse/BrowseAdapterUtils.java b/src/com/android/car/media/browse/BrowseAdapterUtils.java
index 89db87b..661dbd3 100644
--- a/src/com/android/car/media/browse/BrowseAdapterUtils.java
+++ b/src/com/android/car/media/browse/BrowseAdapterUtils.java
@@ -16,12 +16,20 @@
package com.android.car.media.browse;
+import android.content.Context;
import android.view.View;
import android.widget.ProgressBar;
+import androidx.annotation.NonNull;
import androidx.media.utils.MediaConstants;
import com.android.car.apps.common.util.ViewUtils;
+import com.android.car.media.common.CustomBrowseAction;
+import com.android.car.media.common.MediaItemMetadata;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
/**
* Utility class for {@link BrowseViewHolder}
@@ -72,4 +80,36 @@
progressIndicator.setProgress((int) (progress * 100));
}
}
+
+ /**
+ * Builds list of {@link CustomBrowseAction} from the supplied media item.
+ *
+ * @param item - contains the list of action IDs
+ * @param globalBrowseCustomActions - Global actions in root.extras
+ * @return list of actions for item
+ */
+ public static List<CustomBrowseAction> buildBrowseCustomActions(
+ Context mContext,
+ MediaItemMetadata item,
+ @NonNull Map<String, CustomBrowseAction> globalBrowseCustomActions) {
+ int actionsLimit = mContext.getResources()
+ .getInteger(com.android.car.media.common.R.integer.max_custom_actions);
+ if (actionsLimit <= 0) return new ArrayList<>();
+ if (globalBrowseCustomActions.isEmpty()) return new ArrayList<>();
+
+ List<CustomBrowseAction> customActions = new ArrayList<>();
+
+ for (String actionId : item.getBrowseCustomActionIds()) {
+ if (globalBrowseCustomActions.containsKey(actionId)) {
+ CustomBrowseAction customBrowseAction = globalBrowseCustomActions.get(actionId);
+ if (customBrowseAction == null) continue;
+ customActions.add(customBrowseAction);
+ }
+ }
+
+ //Limit item actions to OEM set value
+ actionsLimit = Math.min(customActions.size(), actionsLimit);
+
+ return customActions.subList(0, actionsLimit);
+ }
}
diff --git a/src/com/android/car/media/browse/BrowseViewData.java b/src/com/android/car/media/browse/BrowseViewData.java
index bf64404..18a5edb 100644
--- a/src/com/android/car/media/browse/BrowseViewData.java
+++ b/src/com/android/car/media/browse/BrowseViewData.java
@@ -16,12 +16,12 @@
package com.android.car.media.browse;
-import android.view.View;
-
import androidx.annotation.NonNull;
+import com.android.car.media.common.CustomBrowseAction;
import com.android.car.media.common.MediaItemMetadata;
+import java.util.List;
import java.util.Objects;
/**
@@ -30,54 +30,49 @@
class BrowseViewData {
/** {@link com.android.car.media.common.MediaItemMetadata} associated with this item */
public final MediaItemMetadata mMediaItem;
- /** Item updated by AAOS player, not by media app**/
- public MediaItemMetadata mUpdatedMediaItem = null;
/** View type associated with this item */
@NonNull
public final BrowseItemViewType mViewType;
/** Text associated with this item */
public final CharSequence mText;
- /** Click listener to set for this item */
- public final View.OnClickListener mOnClickListener;
+ List<CustomBrowseAction> mCustomBrowseActions;
+ /** Callback for clicks */
+ public final BrowseViewDataCallback mCallback;
+
+ public interface BrowseViewDataCallback{
+ void onMediaItemClicked(BrowseViewData item);
+ void onBrowseActionClick(CustomBrowseAction action, BrowseViewData browseViewData);
+ void onOverflowClicked(List<CustomBrowseAction> item, BrowseViewData 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 onClickListener optional {@link android.view.View.OnClickListener}
*/
- BrowseViewData(MediaItemMetadata mediaItem, @NonNull BrowseItemViewType viewType,
- View.OnClickListener onClickListener) {
+ BrowseViewData(
+ MediaItemMetadata mediaItem,
+ @NonNull BrowseItemViewType viewType,
+ @NonNull List<CustomBrowseAction> customBrowseActions,
+ BrowseViewDataCallback callback) {
mMediaItem = mediaItem;
mViewType = viewType;
mText = null;
- mOnClickListener = onClickListener;
+ mCallback = callback;
+ mCustomBrowseActions = customBrowseActions;
}
/**
* 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 onClickListener optional {@link android.view.View.OnClickListener}
*/
- BrowseViewData(@NonNull CharSequence text, @NonNull BrowseItemViewType viewType,
- View.OnClickListener onClickListener) {
+ BrowseViewData(
+ @NonNull CharSequence text,
+ @NonNull BrowseItemViewType viewType,
+ @NonNull List<CustomBrowseAction> customBrowseActions,
+ BrowseViewDataCallback callback) {
mText = text;
mViewType = viewType;
mMediaItem = 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;
+ mCallback = callback;
+ mCustomBrowseActions = customBrowseActions;
}
@Override
@@ -87,14 +82,12 @@
BrowseViewData item = (BrowseViewData) o;
return Objects.equals(mMediaItem, item.mMediaItem)
- && Objects.equals(mUpdatedMediaItem, item.mUpdatedMediaItem)
- && mViewType == item.mViewType;
+ && mViewType == item.mViewType
+ && Objects.equals(mCustomBrowseActions, item.mCustomBrowseActions);
}
@Override
public int hashCode() {
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 b9044b1..2ba79db 100644
--- a/src/com/android/car/media/browse/BrowseViewHolder.java
+++ b/src/com/android/car/media/browse/BrowseViewHolder.java
@@ -19,6 +19,7 @@
import android.content.Context;
import android.text.TextUtils;
import android.util.Size;
+import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
@@ -31,9 +32,13 @@
import com.android.car.apps.common.imaging.ImageViewBinder;
import com.android.car.apps.common.util.ViewUtils;
import com.android.car.media.MediaAppConfig;
+import com.android.car.media.R;
+import com.android.car.media.common.CustomBrowseAction;
import com.android.car.media.common.MediaItemMetadata;
+import java.util.ArrayList;
import java.util.List;
+import java.util.stream.Collectors;
/**
* Generic {@link RecyclerView.ViewHolder} to use for all views in the {@link BrowseAdapter}
@@ -50,8 +55,12 @@
private final ImageView mSubTitleExplicitIcon;
private final ProgressBar mProgressbar;
private final ImageView mNewMediaDot;
+ private final ViewGroup mCustomActionsContainer;
+ private final Size mMaxArtSize;
private final ImageViewBinder<MediaItemMetadata.ArtworkRef> mAlbumArtBinder;
+ private final List<ImageViewBinder<CustomBrowseAction.BrowseActionArtRef>>
+ mBrowseActionIcons;
/**
* Creates a {@link BrowseViewHolder} for the given view.
@@ -74,77 +83,93 @@
mProgressbar = itemView.findViewById(com.android.car.media.R.id.browse_item_progress_bar);
mNewMediaDot = itemView.findViewById(com.android.car.media.R.id.browse_item_progress_new);
- Size maxArtSize = MediaAppConfig.getMediaItemsBitmapMaxSize(itemView.getContext());
- mAlbumArtBinder = new ImageViewBinder<>(placeholderType, maxArtSize, mAlbumArt, false);
+ mMaxArtSize = MediaAppConfig.getMediaItemsBitmapMaxSize(itemView.getContext());
+ mAlbumArtBinder = new ImageViewBinder<>(placeholderType, mMaxArtSize, mAlbumArt, false);
+ mCustomActionsContainer =
+ itemView.findViewById(com.android.car.media.R.id.browse_item_actions_container);
+ mBrowseActionIcons = new ArrayList<>();
}
-
/**
* Updates this {@link BrowseViewHolder} with the given data
*/
public void bind(Context context, BrowseViewData data) {
- boolean hasMediaItem = data.mMediaItem != null;
- boolean hasMediaItemExtras = hasMediaItem && data.mMediaItem.getExtras() != null;
- boolean hasUpdatedMediaItemExtras = data.mUpdatedMediaItem != null;
- boolean showSubtitle = hasMediaItem && !TextUtils.isEmpty(data.mMediaItem.getSubtitle());
+ MediaItemMetadata metadata = data.mMediaItem;
+ boolean hasMediaItem = metadata != null;
+ boolean hasMediaItemExtras = hasMediaItem && metadata.getExtras() != null;
+ boolean showSubtitle = hasMediaItem && !TextUtils.isEmpty(metadata.getSubtitle());
+ boolean hasBrowseCustomActions = !data.mCustomBrowseActions.isEmpty();
if (mTitle != null) {
mTitle.setText(data.mText != null ? data.mText :
- hasMediaItem ? data.mMediaItem.getTitle() : null);
+ hasMediaItem ? metadata.getTitle() : null);
}
if (mSubtitle != null) {
- mSubtitle.setText(hasMediaItem ? data.mMediaItem.getSubtitle() : null);
+ mSubtitle.setText(hasMediaItem ? metadata.getSubtitle() : null);
ViewUtils.setVisible(mSubtitle, showSubtitle);
}
- mAlbumArtBinder.setImage(context, hasMediaItem ? data.mMediaItem.getArtworkKey() : null);
+ mAlbumArtBinder.setImage(context, hasMediaItem ? metadata.getArtworkKey() : null);
- if (mContainer != null && data.mOnClickListener != null) {
- mContainer.setOnClickListener(data.mOnClickListener);
+ if (mContainer != null && data.mCallback != null) {
+ mContainer.setOnClickListener(v -> data.mCallback.onMediaItemClicked(data));
}
- ViewUtils.setVisible(mRightArrow, hasMediaItem && data.mMediaItem.isBrowsable());
+ ViewUtils.setVisible(mRightArrow, hasMediaItem && metadata.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();
+ boolean downloaded = hasMediaItem && metadata.isDownloaded();
+ boolean explicit = hasMediaItem && metadata.isExplicit();
ViewUtils.setVisible(mTitleDownloadIcon, !showSubtitle && downloaded);
ViewUtils.setVisible(mTitleExplicitIcon, !showSubtitle && explicit);
ViewUtils.setVisible(mSubTitleDownloadIcon, showSubtitle && downloaded);
ViewUtils.setVisible(mSubTitleExplicitIcon, showSubtitle && explicit);
- if (hasMediaItemExtras && !hasUpdatedMediaItemExtras) {
- bindProgressUI(data.mMediaItem);
+ if (hasMediaItemExtras) {
+ bindProgressUI(metadata);
}
- if (hasUpdatedMediaItemExtras) {
- bindProgressUI(data.mUpdatedMediaItem);
+ if (hasBrowseCustomActions && !metadata.isBrowsable()) {
+ ViewUtils.setVisible(mCustomActionsContainer, true);
+ bindBrowseCustomActions(context, data);
+ } else {
+ ViewUtils.setVisible(mCustomActionsContainer, false);
}
}
/**
- * Handles updated {@link MediaItemMetadata} for a partial bind
- * Partial bind is
- * {@link androidx.recyclerview.widget.ListAdapter#onBindViewHolder(RecyclerView.ViewHolder,
- * int, List)}
+ * Handles updated {@link BrowseViewData} for a partial bind Partial bind is {@link
+ * androidx.recyclerview.widget.ListAdapter#onBindViewHolder(RecyclerView.ViewHolder, int,
+ * List)}
+ *
+ * <p>Called from {@link DiffUtil.ItemCallback#getChangePayload(Object, Object)} where items
+ * same but contents were different and create payload for partial bind
+ *
+ * <p>Or called from {@link
+ * androidx.recyclerview.widget.RecyclerView.AdapterDataObserver#onItemRangeChanged(int, int,
+ * Object)} Where we check if notifyItemChanged() has a payload or not and then call a partial
+ * bind
*
* <p>
- * Called from {@link DiffUtil.ItemCallback#getChangePayload(Object, Object)} where
- * items same but contents were different and create payload for partial bind
- * </p>
- * <p>
- * Or called from
- * {@link androidx.recyclerview.widget.RecyclerView.AdapterDataObserver#onItemRangeChanged(int,
- * int, Object)}
- * Where we check if notifyItemChanged() has a payload or not and then call a partial bind
- * <p/>
*/
- public void update(MediaItemMetadata mediaItemMetadata) {
- boolean hasMediaItem = mediaItemMetadata != null;
- boolean hasMediaItemExtras = hasMediaItem && mediaItemMetadata.getExtras() != null;
- if (hasMediaItemExtras) {
- bindProgressUI(mediaItemMetadata);
+ public void update(
+ BrowseViewData browseViewData, BrowseAdapter.MediaItemUpdateType updateType) {
+ if (updateType == null) {
+ bind(itemView.getContext(), browseViewData);
+ return;
+ }
+ switch (updateType) {
+ case PROGRESS:
+ bindProgressUI(browseViewData.mMediaItem);
+ break;
+ case BROWSE_ACTIONS:
+ Context context = itemView.getContext();
+ bindBrowseCustomActions(context, browseViewData);
+ break;
+ default:
+ bind(itemView.getContext(), browseViewData);
+ break;
}
}
@@ -159,11 +184,55 @@
BrowseAdapterUtils.setPlaybackProgressIndicator(mProgressbar, progress);
}
+ private void bindBrowseCustomActions(Context context, BrowseViewData browseViewData) {
+ mCustomActionsContainer.removeAllViews();
+ mBrowseActionIcons.forEach((it) -> it.maybeCancelLoading(context));
+ mBrowseActionIcons.clear();
+
+ int maxVisibleActions = context.getResources().getInteger(R.integer.max_visible_actions);
+ int numActions = browseViewData.mCustomBrowseActions.size();
+ boolean willOverflow = numActions > maxVisibleActions;
+ int actionsToShow = willOverflow ? Math.max(0, maxVisibleActions - 1) : maxVisibleActions;
+
+ for (CustomBrowseAction customBrowseAction :
+ browseViewData.mCustomBrowseActions.stream()
+ .limit(actionsToShow)
+ .collect(Collectors.toList())) {
+ View customActionView =
+ LayoutInflater.from(context).inflate(R.layout.browse_custom_action, null);
+ customActionView.setOnClickListener(
+ (v) ->
+ browseViewData.mCallback.onBrowseActionClick(
+ customBrowseAction, browseViewData));
+ ImageView imageView = customActionView.findViewById(R.id.browse_item_custom_action);
+ ImageViewBinder<CustomBrowseAction.BrowseActionArtRef> viewBinder =
+ new ImageViewBinder(mMaxArtSize, imageView);
+ viewBinder.setImage(context, customBrowseAction.getArtRef());
+ mBrowseActionIcons.add(viewBinder);
+ mCustomActionsContainer.addView(customActionView);
+ }
+
+ if (willOverflow) {
+ View customActionView =
+ LayoutInflater.from(context)
+ .inflate(R.layout.browse_custom_action, null);
+ customActionView.setOnClickListener(v -> browseViewData.mCallback.onOverflowClicked(
+ browseViewData.mCustomBrowseActions.subList(actionsToShow, numActions),
+ browseViewData));
+ ImageView imageView =
+ customActionView.findViewById(R.id.browse_item_custom_action);
+ imageView.setImageResource(R.drawable.car_ui_icon_overflow_menu);
+ mCustomActionsContainer.addView(customActionView);
+ }
+ }
+
void onViewAttachedToWindow(Context context) {
mAlbumArtBinder.maybeRestartLoading(context);
+ mBrowseActionIcons.forEach((it) -> it.maybeRestartLoading(context));
}
void onViewDetachedFromWindow(Context context) {
mAlbumArtBinder.maybeCancelLoading(context);
+ mBrowseActionIcons.forEach((it) -> it.maybeCancelLoading(context));
}
}
diff --git a/src/com/android/car/media/browse/LimitedBrowseAdapter.java b/src/com/android/car/media/browse/LimitedBrowseAdapter.java
index aea4b49..e1b3699 100644
--- a/src/com/android/car/media/browse/LimitedBrowseAdapter.java
+++ b/src/com/android/car/media/browse/LimitedBrowseAdapter.java
@@ -98,8 +98,9 @@
* Wrapper for {@link BrowseAdapter#updateItemMetaData(MediaItemMetadata)}
* @param mediaItemMetadata
*/
- public void updateItemMetaData(MediaItemMetadata mediaItemMetadata) {
- mBrowseAdapter.updateItemMetaData(mediaItemMetadata);
+ public void updateItemMetaData(MediaItemMetadata mediaItemMetadata,
+ BrowseAdapter.MediaItemUpdateType updateType) {
+ mBrowseAdapter.updateItemMetaData(mediaItemMetadata, updateType);
}
private int validateAnchor() {
diff --git a/src/com/android/car/media/browse/actionbar/ActionsHeader.java b/src/com/android/car/media/browse/actionbar/ActionsHeader.java
new file mode 100644
index 0000000..eafdb28
--- /dev/null
+++ b/src/com/android/car/media/browse/actionbar/ActionsHeader.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.actionbar;
+
+import com.android.car.media.common.CustomBrowseAction;
+
+import java.util.List;
+
+/**
+ * Custom Browse Actions header.
+ * Use {@link BrowseActionsHeader} for implementation.
+ */
+public interface ActionsHeader {
+
+ /** Custom Browse Action click listener */
+ interface ActionClickListener {
+ void onActionClicked(CustomBrowseAction action);
+ }
+
+ /** Overflow menu click listener */
+ interface OverflowClickListener {
+ void onOverFlowCLicked(List<CustomBrowseAction> overflowActions);
+ }
+
+ /** Sets action clicked listener */
+ void setActionClickedListener(ActionClickListener actionClickListener);
+
+ /** Sets overflow menu click listener */
+ void setOnOverflowListener(OverflowClickListener overflowListener);
+
+ /** Sets actions list */
+ void setActions(List<CustomBrowseAction> actions);
+
+ /** Clears all actions */
+ void clearActions();
+
+ /** Sets Actions Header title */
+ void setTitle(CharSequence sourceName);
+
+ /** Sets if toolbar is visible*/
+ void setVisibility(boolean shouldShow);
+
+ /** Returns whether or not the toolbar is shown */
+ boolean isShown();
+
+ /** Returns total height of this view. */
+ int getHeight();
+}
diff --git a/src/com/android/car/media/browse/actionbar/BrowseActionsHeader.java b/src/com/android/car/media/browse/actionbar/BrowseActionsHeader.java
new file mode 100644
index 0000000..044e661
--- /dev/null
+++ b/src/com/android/car/media/browse/actionbar/BrowseActionsHeader.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.actionbar;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Size;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+import com.android.car.apps.common.imaging.ImageViewBinder;
+import com.android.car.media.MediaAppConfig;
+import com.android.car.media.R;
+import com.android.car.media.common.CustomBrowseAction;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Implementation of {@link ActionsHeader}
+ * Use this class to show Custom Browse Actions of parent item.
+ * Supports either secondary toolbar or recycler view header
+ */
+public class BrowseActionsHeader extends LinearLayout implements ActionsHeader {
+ private ActionClickListener mActionClickListener;
+ private OverflowClickListener mOverflowClickListener;
+ private List<CustomBrowseAction> mActions = new ArrayList<>();
+
+ private LinearLayout mActionsContainer;
+
+ public BrowseActionsHeader(Context context, AttributeSet attributeSet) {
+ super(context, attributeSet);
+ initView();
+ }
+
+ private void initView() {
+ inflate(getContext(), R.layout.media_browse_header_item, this);
+ mActionsContainer = (LinearLayout) findViewById(R.id.browse_item_actions_container);
+ }
+
+ @Override
+ public void setActionClickedListener(ActionClickListener actionClickListener) {
+ mActionClickListener = actionClickListener;
+ }
+
+ @Override
+ public void setOnOverflowListener(OverflowClickListener overflowListener) {
+ mOverflowClickListener = overflowListener;
+ }
+
+ @Override
+ public void setActions(List<CustomBrowseAction> actions) {
+ mActions = actions;
+ setHeaderActions(actions);
+ }
+
+ private void setHeaderActions(List<CustomBrowseAction> actions) {
+ mActionsContainer.removeAllViews();
+ final int maxVisibleActions = getResources()
+ .getInteger(R.integer.max_visible_actions_header);
+ final Size mMaxArtSize = MediaAppConfig
+ .getMediaItemsBitmapMaxSize(getContext());
+ for (int i = 0; i < Math.min(maxVisibleActions, actions.size()); i++) {
+ CustomBrowseAction action = actions.get(i);
+ View actionView =
+ LayoutInflater.from(getContext()).inflate(R.layout.browse_custom_action, null);
+ if (i == 0) {
+ actionView.findViewById(R.id.browse_item_custom_action_divider)
+ .setVisibility(View.GONE);
+ }
+ ImageView icon = actionView.findViewById(R.id.browse_item_custom_action);
+ actionView.setOnClickListener(
+ item -> mActionClickListener.onActionClicked(action));
+ ImageViewBinder<CustomBrowseAction.BrowseActionArtRef> imageBinder =
+ new ImageViewBinder<>(mMaxArtSize, icon);
+ imageBinder.setImage(getContext(), action.getArtRef());
+ mActionsContainer.addView(actionView);
+ }
+
+ if (actions.size() > maxVisibleActions) {
+ View actionView =
+ LayoutInflater.from(getContext()).inflate(R.layout.browse_custom_action, null);
+ ImageView icon = actionView.findViewById(R.id.browse_item_custom_action);
+ actionView.setOnClickListener(
+ v ->
+ mOverflowClickListener.onOverFlowCLicked(
+ actions.subList(maxVisibleActions, actions.size())));
+ icon.setImageResource(R.drawable.car_ui_icon_overflow_menu);
+ mActionsContainer.addView(actionView);
+ }
+ }
+
+ @Override
+ public void clearActions() {
+ mActions.clear();
+ setHeaderActions(mActions);
+ }
+
+ @Override
+ public void setTitle(CharSequence sourceName) {
+ //TODO(b/264473064): Add a text view for title
+ }
+
+ @Override
+ public void setVisibility(boolean shouldShow) {
+ setVisibility(shouldShow ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ public boolean isShown() {
+ return getVisibility() == View.VISIBLE;
+ }
+}
diff --git a/src/com/android/car/media/widgets/AppBarController.java b/src/com/android/car/media/widgets/AppBarController.java
index 5d61fa5..babaf29 100644
--- a/src/com/android/car/media/widgets/AppBarController.java
+++ b/src/com/android/car/media/widgets/AppBarController.java
@@ -8,6 +8,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.XmlRes;
import androidx.core.util.Preconditions;
import com.android.car.media.MediaAppConfig;
@@ -91,13 +92,13 @@
protected void onSearchSelection() {}
}
- public AppBarController(Context context, ToolbarController controller) {
+ public AppBarController(Context context, ToolbarController controller, @XmlRes int menuResId,
+ boolean useSourceLogoForAppSelector) {
mToolbarController = controller;
mApplicationContext = context.getApplicationContext();
mMaxTabs = context.getResources().getInteger(R.integer.max_tabs);
- mUseSourceLogoForAppSelector =
- context.getResources().getBoolean(R.bool.use_media_source_logo_for_app_selector);
+ mUseSourceLogoForAppSelector = useSourceLogoForAppSelector;
Intent appSelectorIntent = MediaSource.getSourceSelectorIntent(context, false);
mShowPersistentTabs = context.getResources().getBoolean(R.bool.show_persistent_tabs);
@@ -111,20 +112,23 @@
Map<Integer, MenuItem> menuMap = new HashMap<>();
List<MenuItem> menuItems = MenuItemXmlParserUtil.readMenuItemList(mApplicationContext,
- R.xml.menuitems_browse);
+ menuResId);
menuItems.forEach((item) -> menuMap.put(item.getId(), item));
mSearch = menuMap.get(R.id.menu_item_search);
- Preconditions.checkNotNull(mSearch);
- mSearch.setOnClickListener((menuItem) -> mListener.onSearchSelection());
+ if (mSearch != null) {
+ mSearch.setOnClickListener((menuItem) -> mListener.onSearchSelection());
+ }
mSettings = menuMap.get(R.id.menu_item_setting);
- Preconditions.checkNotNull(mSettings);
- mSettings.setOnClickListener((menuItem) -> mListener.onSettingsSelection());
+ if (mSettings != null) {
+ mSettings.setOnClickListener((menuItem) -> mListener.onSettingsSelection());
+ }
mEqualizer = menuMap.get(R.id.menu_item_equalizer);
- Preconditions.checkNotNull(mEqualizer);
- mEqualizer.setOnClickListener((menuItem) -> mListener.onEqualizerSelection());
+ if (mEqualizer != null) {
+ mEqualizer.setOnClickListener((menuItem) -> mListener.onEqualizerSelection());
+ }
if (mUseSourceLogoForAppSelector) {
menuItems.remove(menuMap.get(R.id.menu_item_selector));
@@ -133,13 +137,22 @@
menuItems.remove(menuMap.get(R.id.menu_item_selector_with_source_logo));
mAppSelector = menuMap.get(R.id.menu_item_selector);
}
- Preconditions.checkNotNull(mAppSelector);
- mAppSelector.setOnClickListener((menuItem) -> context.startActivity(appSelectorIntent));
- mAppSelector.setVisible(appSelectorIntent != null);
+ if (mAppSelector != null) {
+ mAppSelector.setOnClickListener((menuItem) -> context.startActivity(appSelectorIntent));
+ mAppSelector.setVisible(appSelectorIntent != null);
+ }
mToolbarController.setMenuItems(menuItems);
}
+ /** Verifies that all the menus needed in the browse view have been created. */
+ public void checkBrowseMenus() {
+ Preconditions.checkNotNull(mSearch);
+ Preconditions.checkNotNull(mSettings);
+ Preconditions.checkNotNull(mEqualizer);
+ Preconditions.checkNotNull(mAppSelector);
+ }
+
/**
* Sets a listener of this application bar events. In order to avoid memory leaks, consumers
* must reset this reference by setting the listener to null.
@@ -285,6 +298,17 @@
mToolbarController.setTitle(title);
}
+ /** Returns the first menu item matching the given id, or null. */
+ public @Nullable MenuItem getMenuItem(int menuId) {
+ for (MenuItem menuItem : mToolbarController.getMenuItems()) {
+ if (menuItem.getId() == menuId) {
+ return menuItem;
+ }
+ }
+ return null;
+ }
+
+ /** Sets menu items */
public void setMenuItems(List<MenuItem> items) {
mToolbarController.setMenuItems(items);
}
diff --git a/tests/unittests/src/com/android/car/media/browse/BrowseAdapterTests.java b/tests/unittests/src/com/android/car/media/browse/BrowseAdapterTests.java
index 978d79f..fafcbee 100644
--- a/tests/unittests/src/com/android/car/media/browse/BrowseAdapterTests.java
+++ b/tests/unittests/src/com/android/car/media/browse/BrowseAdapterTests.java
@@ -41,6 +41,7 @@
import org.mockito.Mock;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
@@ -65,6 +66,7 @@
public void setup() {
Context context = ApplicationProvider.getApplicationContext();
mBrowseAdapter = new BrowseAdapter(context);
+ mBrowseAdapter.setGlobalCustomActions(Collections.emptyMap());
}
@Test
@@ -73,12 +75,14 @@
ArrayList<BrowseViewData> testListPrev = new ArrayList<>();
testListPrev.add(
- new BrowseViewData("Previous BVD list item", BrowseItemViewType.LIST_ITEM, null));
+ new BrowseViewData(
+ "Previous BVD list item", BrowseItemViewType.LIST_ITEM, null, null));
mBrowseAdapter.onCurrentListChanged(null, testListPrev);
ArrayList<BrowseViewData> testListCurr = new ArrayList<>();
testListCurr.add(
- new BrowseViewData("Current BVD grid item", BrowseItemViewType.GRID_ITEM, null));
+ new BrowseViewData(
+ "Current BVD grid item", BrowseItemViewType.GRID_ITEM, null, null));
mBrowseAdapter.onCurrentListChanged(testListPrev, testListCurr);
verify(mOnListChangedListener, atLeast(1)).onListChanged(mPrevListCaptor.capture(),
@@ -155,11 +159,11 @@
mBrowseAdapter.submitItems(generateParentItem(), itemList);
List<BrowseViewData> items = mBrowseAdapter.getCurrentList();
- items.get(0).mOnClickListener.onClick(null);
- items.get(1).mOnClickListener.onClick(null);
- items.get(2).mOnClickListener.onClick(null);
+ items.get(0).mCallback.onMediaItemClicked(items.get(0));
+ items.get(1).mCallback.onMediaItemClicked(items.get(1));
+ items.get(2).mCallback.onMediaItemClicked(items.get(2));
- verify(mObserver, atLeast(1)).onTitleClicked();
+ verify(mObserver, atLeast(1)).onHeaderItemClicked();
verify(mObserver, atLeast(1)).onBrowsableItemClicked(any());
verify(mObserver, atLeast(1)).onPlayableItemClicked(any());
}
diff --git a/tests/unittests/src/com/android/car/media/browse/BrowseViewHolderTests.java b/tests/unittests/src/com/android/car/media/browse/BrowseViewHolderTests.java
index 6cb9ebe..afb28d1 100644
--- a/tests/unittests/src/com/android/car/media/browse/BrowseViewHolderTests.java
+++ b/tests/unittests/src/com/android/car/media/browse/BrowseViewHolderTests.java
@@ -16,6 +16,8 @@
package com.android.car.media.browse;
+import static com.android.car.media.browse.BrowseItemViewType.ICON_LIST_ITEM;
+import static com.android.car.media.browse.BrowseItemViewType.LIST_ITEM;
import static com.android.car.media.browse.BrowseTestUtils.generateTestItems;
import static junit.framework.Assert.assertEquals;
@@ -33,12 +35,14 @@
import com.android.car.apps.common.imaging.ImageBinder;
import com.android.car.media.R;
+import com.android.car.media.common.CustomBrowseAction;
import com.android.car.media.common.MediaItemMetadata;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.Collections;
import java.util.List;
@RunWith(AndroidJUnit4.class)
@@ -47,17 +51,18 @@
Context mContext;
BrowseViewHolder mBrowseViewHolder;
BrowseViewData mBrowseViewData;
+ List<CustomBrowseAction> mCustomBrowseActions = Collections.emptyList();
View mView;
List<MediaItemMetadata> mItems;
@Before
public void setup() {
mContext = ApplicationProvider.getApplicationContext();
- int layoutId = BrowseItemViewType.ICON_LIST_ITEM.getLayoutId();
+ int layoutId = ICON_LIST_ITEM.getLayoutId();
mView = LayoutInflater.from(mContext).inflate(layoutId, null, false);
mBrowseViewHolder = new BrowseViewHolder(mView, ImageBinder.PlaceholderType.FOREGROUND);
mItems = generateTestItems();
- mBrowseViewData = new BrowseViewData(mItems.get(0), BrowseItemViewType.LIST_ITEM, null);
+ mBrowseViewData = new BrowseViewData(mItems.get(0), LIST_ITEM, mCustomBrowseActions, null);
}
@Test
@@ -91,7 +96,7 @@
@Test
public void onBindDownloaded() {
- mBrowseViewData = new BrowseViewData(mItems.get(2), BrowseItemViewType.LIST_ITEM, null);
+ mBrowseViewData = new BrowseViewData(mItems.get(2), LIST_ITEM, mCustomBrowseActions, null);
mBrowseViewHolder.bind(mContext, mBrowseViewData);
ImageView imageView = mView.findViewById(R.id.download_icon_with_title);
ImageView imageViewSubtitle = mView.findViewById(R.id.download_icon_with_subtitle);
@@ -101,7 +106,7 @@
@Test
public void onBindExplicit() {
- mBrowseViewData = new BrowseViewData(mItems.get(2), BrowseItemViewType.LIST_ITEM, null);
+ mBrowseViewData = new BrowseViewData(mItems.get(2), LIST_ITEM, mCustomBrowseActions, null);
mBrowseViewHolder.bind(mContext, mBrowseViewData);
ImageView imageView = mView.findViewById(R.id.explicit_icon_with_title);
ImageView imageViewSubtitle = mView.findViewById(R.id.explicit_icon_with_subtitle);
@@ -111,25 +116,25 @@
@Test
public void onBindNewIndicator() {
- mBrowseViewData = new BrowseViewData(mItems.get(2), BrowseItemViewType.LIST_ITEM, null);
+ mBrowseViewData = new BrowseViewData(mItems.get(2), LIST_ITEM, mCustomBrowseActions, null);
mBrowseViewHolder.bind(mContext, mBrowseViewData);
ImageView newDot = mView.findViewById(R.id.browse_item_progress_new);
assertEquals(View.VISIBLE, newDot.getVisibility());
- mBrowseViewData = new BrowseViewData(mItems.get(3), BrowseItemViewType.LIST_ITEM, null);
+ mBrowseViewData = new BrowseViewData(mItems.get(3), LIST_ITEM, mCustomBrowseActions, null);
mBrowseViewHolder.bind(mContext, mBrowseViewData);
assertEquals(View.GONE, newDot.getVisibility());
}
@Test
public void onBindProgressUI() {
- mBrowseViewData = new BrowseViewData(mItems.get(2), BrowseItemViewType.LIST_ITEM, null);
+ mBrowseViewData = new BrowseViewData(mItems.get(2), LIST_ITEM, mCustomBrowseActions, null);
mBrowseViewHolder.bind(mContext, mBrowseViewData);
ProgressBar progressBar = mView.findViewById(R.id.browse_item_progress_bar);
assertEquals(View.VISIBLE, progressBar.getVisibility());
assertEquals((int) (mItems.get(2).getProgress() * 100), progressBar.getProgress());
- mBrowseViewData = new BrowseViewData(mItems.get(4), BrowseItemViewType.LIST_ITEM, null);
+ mBrowseViewData = new BrowseViewData(mItems.get(4), LIST_ITEM, mCustomBrowseActions, null);
mBrowseViewHolder.bind(mContext, mBrowseViewData);
assertEquals(View.GONE, progressBar.getVisibility());
assertEquals((int) (mItems.get(4).getProgress() * 100), progressBar.getProgress());
@@ -137,9 +142,11 @@
@Test
public void updateMediaItemMetaData() {
- mBrowseViewData = new BrowseViewData(mItems.get(2), BrowseItemViewType.LIST_ITEM, null);
+ mBrowseViewData = new BrowseViewData(mItems.get(2), LIST_ITEM, mCustomBrowseActions, null);
mBrowseViewHolder.bind(mContext, mBrowseViewData);
- mBrowseViewHolder.update(mItems.get(3));
+ BrowseViewData bvd = new BrowseViewData(mItems.get(3), ICON_LIST_ITEM, mCustomBrowseActions,
+ null);
+ mBrowseViewHolder.update(bvd, BrowseAdapter.MediaItemUpdateType.PROGRESS);
ProgressBar progressBar = mView.findViewById(R.id.browse_item_progress_bar);
ImageView newDot = mView.findViewById(R.id.browse_item_progress_new);
assertEquals(View.VISIBLE, progressBar.getVisibility());