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());