Initial version of 'media-common' library for Car apps.

Bug: 76027628
Test: Manual test. Robotest will be added on b/76016800
Change-Id: I863d45c5d81ccfb905271fde64d40a2df908a685
diff --git a/car-media-common/Android.mk b/car-media-common/Android.mk
new file mode 100644
index 0000000..580bc66
--- /dev/null
+++ b/car-media-common/Android.mk
@@ -0,0 +1,41 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+LOCAL_MODULE := car-media-common
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_PRIVILEGED_MODULE := true
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+LOCAL_STATIC_ANDROID_LIBRARIES += $(ANDROID_SUPPORT_CAR_TARGETS)
+
+LOCAL_USE_AAPT2 := true
+
+include packages/apps/Car/libs/car-apps-common/car-apps-common.mk
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+
diff --git a/car-media-common/AndroidManifest.xml b/car-media-common/AndroidManifest.xml
new file mode 100644
index 0000000..0d1f12c
--- /dev/null
+++ b/car-media-common/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2016 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.car.media.common">
+    <uses-sdk
+            android:minSdkVersion="24"
+            android:targetSdkVersion='24'/>
+</manifest>
diff --git a/car-media-common/car-media-common.mk b/car-media-common/car-media-common.mk
new file mode 100644
index 0000000..52f6f88
--- /dev/null
+++ b/car-media-common/car-media-common.mk
@@ -0,0 +1,44 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#
+# Include this file to utilize the car-media-common's resources and files.
+#
+# Make sure to include it after you've set all your desired LOCAL variables.
+# Note that you must explicitly set your LOCAL_RESOURCE_DIR before including this file.
+#
+# For example:
+#
+#   LOCAL_RESOURCE_DIR := \
+#        $(LOCAL_PATH)/res
+#
+#   include packages/apps/Car/libs/car-media-common/car-media-common
+#
+
+# Check that LOCAL_RESOURCE_DIR is defined
+ifeq (,$(LOCAL_RESOURCE_DIR))
+$(error LOCAL_RESOURCE_DIR must be defined)
+endif
+
+# Include car-apps-common
+ifeq (,$(findstring car-apps-common, $(LOCAL_STATIC_ANDROID_LIBRARIES)))
+LOCAL_STATIC_ANDROID_LIBRARIES += car-apps-common
+endif
+
+# Include car-media-common
+ifeq (,$(findstring car-media-common, $(LOCAL_STATIC_ANDROID_LIBRARIES)))
+LOCAL_STATIC_ANDROID_LIBRARIES += car-media-common
+endif
diff --git a/car-media-common/res/anim/progress_indeterminate_material.xml b/car-media-common/res/anim/progress_indeterminate_material.xml
new file mode 100644
index 0000000..9118382
--- /dev/null
+++ b/car-media-common/res/anim/progress_indeterminate_material.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2018, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android" >
+    <objectAnimator
+        android:duration="1333"
+        android:interpolator="@anim/trim_start_interpolator"
+        android:propertyName="trimPathStart"
+        android:repeatCount="-1"
+        android:valueFrom="0"
+        android:valueTo="0.75"
+        android:valueType="floatType" />
+    <objectAnimator
+        android:duration="1333"
+        android:interpolator="@anim/trim_end_interpolator"
+        android:propertyName="trimPathEnd"
+        android:repeatCount="-1"
+        android:valueFrom="0"
+        android:valueTo="0.75"
+        android:valueType="floatType" />
+    <objectAnimator
+        android:duration="1333"
+        android:interpolator="@android:anim/linear_interpolator"
+        android:propertyName="trimPathOffset"
+        android:repeatCount="-1"
+        android:valueFrom="0"
+        android:valueTo="0.25"
+        android:valueType="floatType" />
+</set>
diff --git a/car-media-common/res/anim/progress_indeterminate_rotation_material.xml b/car-media-common/res/anim/progress_indeterminate_rotation_material.xml
new file mode 100644
index 0000000..992c5e8
--- /dev/null
+++ b/car-media-common/res/anim/progress_indeterminate_rotation_material.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2018, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+    android:duration="6665"
+    android:interpolator="@android:anim/linear_interpolator"
+    android:propertyName="rotation"
+    android:repeatCount="-1"
+    android:valueFrom="0"
+    android:valueTo="720"
+    android:valueType="floatType" />
diff --git a/car-media-common/res/anim/trim_end_interpolator.xml b/car-media-common/res/anim/trim_end_interpolator.xml
new file mode 100644
index 0000000..e0a6475
--- /dev/null
+++ b/car-media-common/res/anim/trim_end_interpolator.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2018, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+      android:pathData="C0.2,0 0.1,1 0.5, 1 L 1,1" />
diff --git a/car-media-common/res/anim/trim_start_interpolator.xml b/car-media-common/res/anim/trim_start_interpolator.xml
new file mode 100644
index 0000000..7cb46d8
--- /dev/null
+++ b/car-media-common/res/anim/trim_start_interpolator.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2018, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+      android:pathData="L0.5,0 C 0.7,0 0.6,1 1, 1" />
diff --git a/car-media-common/res/drawable-hdpi/progressbar.9.png b/car-media-common/res/drawable-hdpi/progressbar.9.png
new file mode 100644
index 0000000..784de64
--- /dev/null
+++ b/car-media-common/res/drawable-hdpi/progressbar.9.png
Binary files differ
diff --git a/car-media-common/res/drawable-mdpi/progressbar.9.png b/car-media-common/res/drawable-mdpi/progressbar.9.png
new file mode 100644
index 0000000..268a32c
--- /dev/null
+++ b/car-media-common/res/drawable-mdpi/progressbar.9.png
Binary files differ
diff --git a/car-media-common/res/drawable-xhdpi/progressbar.9.png b/car-media-common/res/drawable-xhdpi/progressbar.9.png
new file mode 100644
index 0000000..b0b3442
--- /dev/null
+++ b/car-media-common/res/drawable-xhdpi/progressbar.9.png
Binary files differ
diff --git a/car-media-common/res/drawable-xxhdpi/progressbar.9.png b/car-media-common/res/drawable-xxhdpi/progressbar.9.png
new file mode 100644
index 0000000..f78c915
--- /dev/null
+++ b/car-media-common/res/drawable-xxhdpi/progressbar.9.png
Binary files differ
diff --git a/car-media-common/res/drawable/car_fab_empty_foreground.xml b/car-media-common/res/drawable/car_fab_empty_foreground.xml
new file mode 100644
index 0000000..649bc4d
--- /dev/null
+++ b/car-media-common/res/drawable/car_fab_empty_foreground.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2018, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<ripple
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:radius="0dp"
+    android:color="@color/car_card" />
diff --git a/car-media-common/res/drawable/car_playback_bottom_scrim.xml b/car-media-common/res/drawable/car_playback_bottom_scrim.xml
new file mode 100644
index 0000000..e216332
--- /dev/null
+++ b/car-media-common/res/drawable/car_playback_bottom_scrim.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2018, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<shape
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <gradient
+        android:startColor="#000000"
+        android:endColor="#00000000"
+        android:angle="90" />
+</shape>
\ No newline at end of file
diff --git a/car-media-common/res/drawable/ic_pause.xml b/car-media-common/res/drawable/ic_pause.xml
new file mode 100644
index 0000000..a165066
--- /dev/null
+++ b/car-media-common/res/drawable/ic_pause.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2018, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="56dp"
+    android:height="56dp"
+    android:viewportWidth="48"
+    android:viewportHeight="48">
+
+    <path
+        android:fillColor="#000000"
+        android:pathData="M12 38h8V10h-8v28zm16-28v28h8V10h-8z" />
+    <path
+        android:pathData="M0 0h48v48H0z" />
+</vector>
diff --git a/car-media-common/res/drawable/ic_play_arrow.xml b/car-media-common/res/drawable/ic_play_arrow.xml
new file mode 100644
index 0000000..deea6a8
--- /dev/null
+++ b/car-media-common/res/drawable/ic_play_arrow.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2018, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="56dp"
+    android:height="56dp"
+    android:viewportWidth="48"
+    android:viewportHeight="48">
+
+    <path
+        android:pathData="M-838-2232H562v3600H-838z" />
+    <path
+        android:fillColor="#000000"
+        android:pathData="M16 10v28l22-14z" />
+    <path
+        android:pathData="M0 0h48v48H0z" />
+</vector>
diff --git a/car-media-common/res/drawable/ic_play_arrow_off.xml b/car-media-common/res/drawable/ic_play_arrow_off.xml
new file mode 100644
index 0000000..da7312e
--- /dev/null
+++ b/car-media-common/res/drawable/ic_play_arrow_off.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2018, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="56dp"
+    android:height="56dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <group
+            android:translateX="3.000000"
+            android:translateY="3.000000">
+        <path
+            android:fillColor="#000000"
+            android:strokeWidth="1"
+            android:pathData="M16,9 L7.249,3.431 L14.048,10.242 L16,9 Z" />
+        <path
+            android:fillColor="#000000"
+            android:strokeWidth="1"
+            android:pathData="M18,16.73 L1.27,0 L0,1.27 L5,6.27 L5,16 L10.946,12.216 L16.73,18 L18,16.73 Z" />
+    </group>
+</vector>
diff --git a/car-media-common/res/drawable/ic_play_pause_stop_animated.xml b/car-media-common/res/drawable/ic_play_pause_stop_animated.xml
new file mode 100644
index 0000000..131de42
--- /dev/null
+++ b/car-media-common/res/drawable/ic_play_pause_stop_animated.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2018, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<selector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:car="http://schemas.android.com/apk/res-auto" >
+  <item
+      car:state_pause="true"
+      android:drawable="@drawable/ic_pause" />
+  <item
+      car:state_stop="true"
+      android:drawable="@drawable/ic_stop" />
+  <item
+      car:state_play="true"
+      android:drawable="@drawable/ic_play_arrow" />
+  <item
+      car:state_disabled="true"
+      android:drawable="@drawable/ic_play_arrow_off" />
+  <item
+      android:drawable="@drawable/ic_play_arrow" />
+</selector>
diff --git a/car-media-common/res/drawable/ic_skip_next.xml b/car-media-common/res/drawable/ic_skip_next.xml
new file mode 100644
index 0000000..6edf14b
--- /dev/null
+++ b/car-media-common/res/drawable/ic_skip_next.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2018, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="56dp"
+    android:height="56dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <path
+        android:fillColor="#000000"
+        android:pathData="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z" />
+    <path
+        android:pathData="M0 0h24v24H0z" />
+</vector>
diff --git a/car-media-common/res/drawable/ic_skip_previous.xml b/car-media-common/res/drawable/ic_skip_previous.xml
new file mode 100644
index 0000000..5c49202
--- /dev/null
+++ b/car-media-common/res/drawable/ic_skip_previous.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2018, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="56dp"
+    android:height="56dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <path
+        android:fillColor="#000000"
+        android:pathData="M6 6h2v12H6zm3.5 6l8.5 6V6z" />
+    <path
+        android:pathData="M0 0h24v24H0z" />
+</vector>
diff --git a/car-media-common/res/drawable/ic_stop.xml b/car-media-common/res/drawable/ic_stop.xml
new file mode 100644
index 0000000..39afc54
--- /dev/null
+++ b/car-media-common/res/drawable/ic_stop.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2018, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="56dp"
+    android:height="56dp"
+    android:viewportWidth="48"
+    android:viewportHeight="48">
+
+    <path
+        android:pathData="M0 0h48v48H0z" />
+    <path
+        android:fillColor="#000000"
+        android:pathData="M12 12h24v24H12z" />
+</vector>
diff --git a/car-media-common/res/drawable/ic_tracklist.xml b/car-media-common/res/drawable/ic_tracklist.xml
new file mode 100644
index 0000000..ac1522b
--- /dev/null
+++ b/car-media-common/res/drawable/ic_tracklist.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2018, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="56dp"
+    android:height="56dp"
+    android:viewportWidth="48"
+    android:viewportHeight="48">
+
+    <path
+        android:pathData="M0 0h48v48H0z" />
+    <path
+        android:fillColor="#000000"
+        android:pathData="M30 12H6v4h24v-4zm0 8H6v4h24v-4zM6
+32h16v-4H6v4zm28-20v16.37c-.63-.23-1.29-.37-2-.37-3.31 0-6 2.69-6 6s2.69 6 6 6
+6-2.69 6-6V16h6v-4H34z" />
+</vector>
diff --git a/car-media-common/res/drawable/music_buffering.xml b/car-media-common/res/drawable/music_buffering.xml
new file mode 100644
index 0000000..13136d4
--- /dev/null
+++ b/car-media-common/res/drawable/music_buffering.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2018, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+     android:drawable="@drawable/vector_drawable_progress_bar_medium_thin" >
+    <target
+        android:name="progressBar"
+        android:animation="@anim/progress_indeterminate_material" />
+    <target
+        android:name="root"
+        android:animation="@anim/progress_indeterminate_rotation_material" />
+</animated-vector>
diff --git a/car-media-common/res/drawable/seekbar_background.xml b/car-media-common/res/drawable/seekbar_background.xml
new file mode 100644
index 0000000..fcd06a2
--- /dev/null
+++ b/car-media-common/res/drawable/seekbar_background.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2018, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:id="@android:id/background"
+        android:drawable="@android:color/transparent" />
+
+    <item android:id="@android:id/secondaryProgress">
+        <scale android:scaleWidth="100%"
+            android:drawable="@android:color/transparent" />
+    </item>
+
+    <item android:id="@android:id/progress">
+        <scale android:scaleWidth="100%"
+            android:drawable="@drawable/progressbar" />
+    </item>
+
+</layer-list>
diff --git a/car-media-common/res/drawable/vector_drawable_progress_bar_medium_thin.xml b/car-media-common/res/drawable/vector_drawable_progress_bar_medium_thin.xml
new file mode 100644
index 0000000..bcda74d
--- /dev/null
+++ b/car-media-common/res/drawable/vector_drawable_progress_bar_medium_thin.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2018, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:height="48dp"
+        android:width="48dp"
+        android:viewportHeight="48"
+        android:viewportWidth="48" >
+    <group
+        android:name="root"
+        android:translateX="24.0"
+        android:translateY="24.0" >
+        <path
+            android:name="progressBar"
+            android:fillColor="#00000000"
+            android:pathData="M0, 0 m 0, -19 a 19,19 0 1,1 0,38 a 19,19 0 1,1 0,-38"
+            android:strokeColor="?android:attr/colorControlActivated"
+            android:strokeLineCap="square"
+            android:strokeLineJoin="miter"
+            android:strokeWidth="2"
+            android:trimPathEnd="0"
+            android:trimPathOffset="0"
+            android:trimPathStart="0" />
+    </group>
+</vector>
diff --git a/car-media-common/res/layout/car_play_pause_stop_button_layout.xml b/car-media-common/res/layout/car_play_pause_stop_button_layout.xml
new file mode 100644
index 0000000..22b3750
--- /dev/null
+++ b/car-media-common/res/layout/car_play_pause_stop_button_layout.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2018, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/play_pause_container"
+    android:clipChildren="false"
+    android:focusable="false"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content">
+    <!-- The invisible foreground ripple stops Android O from drawing an ugly square over the play
+    button -->
+    <com.android.car.media.common.PlayPauseStopImageView
+        android:id="@+id/play_pause_stop"
+        style="@style/FabStyle.Large"
+        android:foreground="@drawable/car_fab_empty_foreground"
+        android:src="@drawable/ic_play_pause_stop_animated"/>
+    <ProgressBar
+        android:id="@+id/spinner"
+        android:layout_width="@dimen/car_action_bar_fab_spinner_size"
+        android:layout_height="@dimen/car_action_bar_fab_spinner_size"
+        android:layout_gravity="center"
+        android:padding="9dp"
+        android:indeterminateDrawable="@drawable/music_buffering"
+        android:focusable="false"
+        android:visibility="invisible" />
+</FrameLayout>
diff --git a/car-media-common/res/layout/car_playback_fragment.xml b/car-media-common/res/layout/car_playback_fragment.xml
new file mode 100644
index 0000000..c2fd9af
--- /dev/null
+++ b/car-media-common/res/layout/car_playback_fragment.xml
@@ -0,0 +1,143 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2018, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_height="match_parent"
+    android:layout_width="match_parent">
+
+    <ImageView
+        android:id="@+id/album_background"
+        android:background="@color/car_dark_blue_grey_800"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:scaleType="centerCrop" />
+
+    <View
+        android:id="@+id/playback_scrim"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@color/car_dark_blue_grey_900"
+        android:alpha="@dimen/playback_initial_scrim_alpha"/>
+
+    <View
+        android:id="@+id/playback_scrim_bottom"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/car_action_bar_height_scrim"
+        android:layout_alignParentBottom="true"
+        android:background="@drawable/car_playback_bottom_scrim"/>
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:fitsSystemWindows="true">
+
+        <LinearLayout
+            android:id="@+id/metadata"
+            android:orientation="horizontal"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:paddingBottom="@dimen/car_action_bar_height"
+            android:layout_centerVertical="true">
+
+            <ImageView
+                android:id="@+id/album_art"
+                android:contentDescription="@string/album_art"
+                android:layout_marginStart="@dimen/car_keyline_1"
+                android:layout_marginEnd="@dimen/car_keyline_1"
+                android:layout_width="200dp"
+                android:layout_height="190dp"
+                android:scaleType="centerCrop"
+                android:transitionName="@string/album_art"/>
+
+            <LinearLayout
+                android:orientation="vertical"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_marginEnd="@dimen/car_keyline_1"
+                android:layout_weight="1">
+
+                <TextView
+                    android:id="@+id/title"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    style="@style/TextAppearance.Car.Body1"
+                    android:textColor="@color/car_headline1_light"
+                    android:gravity="start"
+                    android:maxLines="@integer/playback_title_text_max_lines"
+                    android:ellipsize="end"
+                    android:textAlignment="center"
+                    android:includeFontPadding="false"/>
+
+                <TextView
+                    android:id="@+id/subtitle"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    style="@style/TextAppearance.Car.Body2"
+                    android:textColor="@color/car_headline1_light"
+                    android:layout_marginTop="12dp"
+                    android:gravity="start"
+                    android:ellipsize="end"
+                    android:singleLine="true"
+                    android:textAlignment="center"/>
+                <TextView
+                    android:id="@+id/description"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    style="@style/TextAppearance.Car.Body2"
+                    android:visibility="invisible"
+                    android:textColor="@color/car_headline1_light"
+                    android:layout_marginTop="@dimen/playback_secondary_text_margin_top"
+                    android:gravity="start"
+                    android:ellipsize="end"
+                    android:singleLine="true"
+                    android:textAlignment="center"/>
+                <Space
+                    android:layout_width="match_parent"
+                    android:layout_height="0dp"
+                    android:layout_weight="1"/>
+                <SeekBar
+                    android:id="@+id/seek_bar"
+                    android:layout_width="match_parent"
+                    android:layout_height="@dimen/car_seekbar_height"
+                    android:focusable="false"
+                    android:visibility="invisible"
+                    android:paddingStart="0dp"
+                    android:paddingEnd="0dp"
+                    android:progressDrawable="@drawable/seekbar_background"
+                    android:background="@color/car_grey_50"
+                    android:thumb="@null"/>
+
+            </LinearLayout>
+
+        </LinearLayout>
+
+        <com.android.car.media.common.PlaybackControls
+            android:id="@+id/playback_controls"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="@dimen/car_keyline_1"
+            android:layout_marginEnd="@dimen/car_keyline_1"
+            android:layout_marginBottom="@dimen/car_action_bar_padding_bottom"
+            android:layout_alignParentBottom="true"
+            android:background="@android:color/transparent"
+            app:cardBackgroundColor="@android:color/transparent"
+            app:columns="3"/>
+
+    </RelativeLayout>
+
+</RelativeLayout>
diff --git a/car-media-common/res/values/attrs.xml b/car-media-common/res/values/attrs.xml
new file mode 100644
index 0000000..588bfa3
--- /dev/null
+++ b/car-media-common/res/values/attrs.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2018, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<resources>
+    <declare-styleable name="PlayPauseStopState">
+        <attr name="state_pause" format="boolean"/>
+        <attr name="state_stop" format="boolean"/>
+        <attr name="state_play" format="boolean"/>
+        <attr name="state_disabled" format="boolean"/>
+    </declare-styleable>
+
+    <declare-styleable name="PlaybackControls">
+        <attr name="columns" format="integer" />
+        <attr name="show_expand_button" format="boolean"/>
+    </declare-styleable>
+</resources>
diff --git a/car-media-common/res/values/dimens.xml b/car-media-common/res/values/dimens.xml
new file mode 100644
index 0000000..2d24bcc
--- /dev/null
+++ b/car-media-common/res/values/dimens.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2018, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<resources>
+    <!-- Music -->
+    <item name="media_scrim_alpha" format="float" type="dimen">0.6</item>
+    <item name="media_scrim_darkened_alpha" format="float" type="dimen">0.9</item>
+    <dimen name="apps_max_content_width">748dp</dimen>
+
+    <!-- Music now playing screen -->
+    <dimen name="music_action_icon_inset">0dp</dimen>
+    <dimen name="music_action_ripple_inset">32dp</dimen>
+    <dimen name="now_playing_metadata_top_margin">0dp</dimen>
+
+    <dimen name="missing_permission_icon_size">208dp</dimen>
+
+    <dimen name="controls_tap_target_width">64dp</dimen>
+    <dimen name="controls_tap_target_height">64dp</dimen>
+    <dimen name="controls_margin">46dp</dimen>
+    <dimen name="controls_spacing_inner">16dp</dimen>
+    <dimen name="controls_spacing_outer">81dp</dimen>
+
+    <dimen name="car_action_bar_fab_spinner_size">128dp</dimen>
+    <dimen name="car_action_bar_height_scrim">256dp</dimen>
+    <dimen name="car_action_bar_padding_bottom">@dimen/car_padding_2</dimen>
+
+    <!-- Playback -->
+    <item name="playback_initial_scrim_alpha" format="float" type="dimen">0.6</item>
+    <dimen name="playback_secondary_text_margin_top">16dp</dimen>
+
+    <!-- Fab -->
+    <dimen name="car_fab_focused_stroke_width">8dp</dimen>
+    <dimen name="car_fab_focused_growth">1.2dp</dimen>
+    <dimen name="car_fab_elevation">8dp</dimen>
+    <dimen name="car_fab_large_size">96dp</dimen>
+    <dimen name="car_fab_large_padding">27dp</dimen>
+    <dimen name="car_fab_small_size">68dp</dimen>
+    <dimen name="car_fab_small_padding">0dp</dimen>
+
+</resources>
diff --git a/car-media-common/res/values/integers.xml b/car-media-common/res/values/integers.xml
new file mode 100644
index 0000000..9ea7fa9
--- /dev/null
+++ b/car-media-common/res/values/integers.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2018, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<resources>
+    <!-- The amount of time it takes for a new album image to fade in. -->
+    <integer name="new_album_art_fade_in_duration">250</integer>
+
+    <!-- The maximum number of lines for the title of a currently playing media item. -->
+    <integer name="media_title_max_lines">2</integer>
+
+    <!-- The maximum number of lines for the artist of a currently playing media item. -->
+    <integer name="media_artist_max_lines">1</integer>
+
+    <!-- Playback View -->
+    <integer name="playback_title_text_max_lines">2</integer>
+
+</resources>
diff --git a/car-media-common/res/values/strings.xml b/car-media-common/res/values/strings.xml
new file mode 100644
index 0000000..c80ac64
--- /dev/null
+++ b/car-media-common/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2018, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Content description of album art image [CHAR LIMIT=50] -->
+    <string name="album_art">Album Art</string>
+</resources>
diff --git a/car-media-common/res/values/styles.xml b/car-media-common/res/values/styles.xml
new file mode 100644
index 0000000..3dbcf1a
--- /dev/null
+++ b/car-media-common/res/values/styles.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2018, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <!-- Fab -->
+    <style name="FabStyle">
+        <item name="android:layout_gravity">center</item>
+        <item name="android:tint">@color/car_tint_light</item>
+        <item name="android:elevation">@dimen/car_fab_elevation</item>
+        <item name="android:focusable">true</item>
+        <item name="android:stateListAnimator">@anim/car_fab_state_list_animator</item>
+    </style>
+
+    <style name="FabStyle.Large" parent="FabStyle">
+        <item name="android:layout_width">@dimen/car_fab_large_size</item>
+        <item name="android:layout_height">@dimen/car_fab_large_size</item>
+        <item name="android:scaleType">fitCenter</item>
+        <item name="android:padding">@dimen/car_fab_large_padding</item>
+    </style>
+
+</resources>
diff --git a/car-media-common/src/com/android/car/media/common/CustomPlaybackAction.java b/car-media-common/src/com/android/car/media/common/CustomPlaybackAction.java
new file mode 100644
index 0000000..2e05d80
--- /dev/null
+++ b/car-media-common/src/com/android/car/media/common/CustomPlaybackAction.java
@@ -0,0 +1,51 @@
+/*
+ * 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.common;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+
+/**
+ * Abstract representation of a custom playback action. A custom playback action represents a
+ * visual element that can be used to trigger playback actions not included in the standard
+ * {@link PlaybackControls} class.
+ * Custom actions for the current media source are exposed through
+ * {@link PlaybackModel#getCustomActions()}
+ */
+public class CustomPlaybackAction {
+    /** Icon to display for this custom action */
+    @NonNull
+    public final Drawable mIcon;
+    /** Action identifier used to request this action to the media service */
+    @NonNull
+    public final String mAction;
+    /** Any additional information to send along with the action identifier */
+    @Nullable
+    public final Bundle mExtras;
+
+    /**
+     * Creates a custom action
+     */
+    public CustomPlaybackAction(@NonNull Drawable icon, @NonNull String action,
+            @Nullable Bundle extras) {
+        mIcon = icon;
+        mAction = action;
+        mExtras = extras;
+    }
+}
diff --git a/car-media-common/src/com/android/car/media/common/MediaItemMetadata.java b/car-media-common/src/com/android/car/media/common/MediaItemMetadata.java
new file mode 100644
index 0000000..4d21dc7
--- /dev/null
+++ b/car-media-common/src/com/android/car/media/common/MediaItemMetadata.java
@@ -0,0 +1,125 @@
+/*
+ * 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.common;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.MediaDescription;
+import android.media.MediaMetadata;
+import android.media.session.MediaSession;
+
+/**
+ * Abstract representation of a media item metadata.
+ */
+public class MediaItemMetadata {
+    private final MediaMetadata mMediaMetadata;
+    private final MediaDescription mMediaDescription;
+    private final Context mContext;
+
+    /** Media item title */
+    @Nullable
+    public final CharSequence mTitle;
+
+    /** Media item subtitle */
+    @Nullable
+    public final CharSequence mSubtitle;
+
+    /** Media item description */
+    @Nullable
+    public final CharSequence mDescription;
+
+    /** Creates an instance based on the individual pieces of data */
+    public MediaItemMetadata(Context context, MediaMetadata metadata) {
+        MediaDescription description = metadata.getDescription();
+        mContext = context;
+        mTitle = description.getTitle();
+        mSubtitle = description.getSubtitle();
+        mDescription = description.getDescription();
+        mMediaMetadata = metadata;
+        mMediaDescription = metadata.getDescription();
+    }
+
+    /** Creates an instance based on a {@link MediaSession.QueueItem} */
+    public MediaItemMetadata(Context context, MediaSession.QueueItem queueItem) {
+        MediaDescription description = queueItem.getDescription();
+        mContext = context;
+        mTitle = description.getTitle();
+        mSubtitle = description.getSubtitle();
+        mDescription = description.getDescription();
+        mMediaMetadata = null;
+        mMediaDescription = queueItem.getDescription();
+    }
+
+    /**
+     * @return a {@link Drawable} corresponding to the album art of this item.
+     */
+    @Nullable
+    public Drawable getAlbumArt() {
+        Drawable drawable = null;
+        if (mMediaMetadata != null) {
+            drawable = getAlbumArtFromMetadata(mContext, mMediaMetadata);
+        }
+        if (drawable == null && mMediaDescription != null) {
+            drawable = getAlbumArtFromDescription(mContext, mMediaDescription);
+        }
+        // TODO(b/76099191): Implement caching
+        return drawable;
+    }
+
+    private static Drawable getAlbumArtFromMetadata(Context context, MediaMetadata metadata) {
+        Bitmap icon = getMetadataBitmap(metadata);
+        if (icon != null) {
+            return new BitmapDrawable(context.getResources(), icon);
+        } else {
+            // TODO(b/76099191): get icon from metadata URIs
+        }
+        return null;
+    }
+
+    private static Drawable getAlbumArtFromDescription(Context context,
+            MediaDescription description) {
+        Bitmap icon = description.getIconBitmap();
+        if (icon != null) {
+            return new BitmapDrawable(context.getResources(), icon);
+        } else {
+            // TODO(b/76099191) get icon from description icon URI
+        }
+        return null;
+    }
+
+    private static final String[] PREFERRED_BITMAP_ORDER = {
+            MediaMetadata.METADATA_KEY_ALBUM_ART,
+            MediaMetadata.METADATA_KEY_ART,
+            MediaMetadata.METADATA_KEY_DISPLAY_ICON
+    };
+
+    @Nullable
+    private static Bitmap getMetadataBitmap(@NonNull MediaMetadata metadata) {
+        // Get the best art bitmap we can find
+        for (String bitmapKey : PREFERRED_BITMAP_ORDER) {
+            Bitmap bitmap = metadata.getBitmap(bitmapKey);
+            if (bitmap != null) {
+                return bitmap;
+            }
+        }
+        return null;
+    }
+}
diff --git a/car-media-common/src/com/android/car/media/common/PlayPauseStopImageView.java b/car-media-common/src/com/android/car/media/common/PlayPauseStopImageView.java
new file mode 100644
index 0000000..16adc5b
--- /dev/null
+++ b/car-media-common/src/com/android/car/media/common/PlayPauseStopImageView.java
@@ -0,0 +1,129 @@
+/*
+ * 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.common;
+
+import android.content.Context;
+import android.graphics.PorterDuff;
+import android.support.annotation.IntDef;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.widget.ImageView;
+
+import com.android.car.apps.common.FabDrawable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Custom {@link android.widget.ImageButton} that has four custom states:
+ * <ul>
+ * <li>state_playing</li>
+ * <li>state_paused</li>
+ * <li>state_stopped</li>
+ * <li>state_disabled</li>
+ * </ul>
+ */
+public class PlayPauseStopImageView extends ImageView {
+    private static final String TAG = "PlayPauseStopImageView";
+
+    private static final int[] STATE_PAUSE = {R.attr.state_pause};
+    private static final int[] STATE_STOP = {R.attr.state_stop};
+    private static final int[] STATE_PLAY = {R.attr.state_play};
+    private static final int[] STATE_DISABLED = {R.attr.state_disabled};
+
+    /**
+     * Possible states of this view
+     */
+    @IntDef({ACTION_PLAY, ACTION_STOP, ACTION_PAUSE, ACTION_DISABLED})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Action {}
+
+    /** Used when no action can be executed at this time */
+    public static final int ACTION_DISABLED = 0;
+    /** Used when the media source is ready to start playing */
+    public static final int ACTION_PLAY = 1;
+    /** Used when the media source is playing and it only support stop action */
+    public static final int ACTION_STOP = 2;
+    /** Used when the media source is playing and it supports pause action */
+    public static final int ACTION_PAUSE = 3;
+
+    private int mAction = ACTION_DISABLED;
+
+    /**
+     * Constructs an instance of this view
+     */
+    public PlayPauseStopImageView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        setBackground(new FabDrawable(context));
+    }
+
+    /**
+     * Sets the action to display on this view
+     *
+     * @param action one of {@link Action}
+     */
+    public void setAction(@Action int action) {
+        mAction = action;
+        refreshDrawableState();
+    }
+
+    /**
+     * @return currently selected action
+     */
+    public int getAction() {
+        return mAction;
+    }
+
+    @Override
+    public int[] onCreateDrawableState(int extraSpace) {
+        // + 1 so we can potentially add our custom PlayState
+        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+
+        switch(mAction) {
+            case ACTION_PLAY:
+                mergeDrawableStates(drawableState, STATE_PLAY);
+                break;
+            case ACTION_STOP:
+                mergeDrawableStates(drawableState, STATE_STOP);
+                break;
+            case ACTION_PAUSE:
+                mergeDrawableStates(drawableState, STATE_PAUSE);
+                break;
+            case ACTION_DISABLED:
+                mergeDrawableStates(drawableState, STATE_DISABLED);
+                break;
+            default:
+                Log.e(TAG, "Unknown action: " + mAction);
+        }
+        if (getBackground() != null) {
+            getBackground().setState(drawableState);
+        }
+        return drawableState;
+    }
+
+    /**
+     * Updates the primary color of this view.
+     * @param color fill or main color
+     * @param tintColor contrast color
+     */
+    public void setPrimaryActionColor(int color, int tintColor) {
+        ((FabDrawable) getBackground()).setFabAndStrokeColor(color);
+        if (getDrawable() != null) {
+            getDrawable().setColorFilter(tintColor, PorterDuff.Mode.SRC_IN);
+        }
+    }
+}
diff --git a/car-media-common/src/com/android/car/media/common/PlaybackControls.java b/car-media-common/src/com/android/car/media/common/PlaybackControls.java
new file mode 100644
index 0000000..8900ff3
--- /dev/null
+++ b/car-media-common/src/com/android/car/media/common/PlaybackControls.java
@@ -0,0 +1,217 @@
+/*
+ * 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.common;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ProgressBar;
+
+import com.android.car.apps.common.ColorChecker;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.car.widget.ActionBar;
+
+/**
+ * Custom view that can be used to display playback controls. It accepts a {@link PlaybackModel}
+ * as its data source, automatically reacting to changes in playback state.
+ */
+public class PlaybackControls extends ActionBar {
+    private static final String TAG = "PlaybackView";
+
+    private PlayPauseStopImageView mPlayPauseStopImageView;
+    private View mPlayPauseStopImageContainer;
+    private ProgressBar mSpinner;
+    private Context mContext;
+    private ImageButton mSkipPrevButton;
+    private ImageButton mSkipNextButton;
+    private List<ImageButton> mCustomActionButtons = new ArrayList<>();
+    private PlaybackModel mModel;
+    private PlaybackModel.PlaybackObserver mObserver = new PlaybackModel.PlaybackObserver() {
+        @Override
+        protected void onPlaybackStateChanged() {
+            updateState();
+            updateCustomActions();
+        }
+
+        @Override
+        protected void onSourceChanged() {
+            updateState();
+            updateCustomActions();
+            updateAccentColor();
+        }
+    };
+
+    /** Creates a {@link PlaybackControls} view */
+    public PlaybackControls(Context context) {
+        this(context, null, 0, 0);
+    }
+
+    /** Creates a {@link PlaybackControls} view */
+    public PlaybackControls(Context context, AttributeSet attrs) {
+        this(context, attrs, 0, 0);
+    }
+
+    /** Creates a {@link PlaybackControls} view */
+    public PlaybackControls(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    /** Creates a {@link PlaybackControls} view */
+    public PlaybackControls(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        init(context);
+    }
+
+    /**
+     * Sets the {@link PlaybackModel} to use as the view model for this view.
+     */
+    public void setModel(PlaybackModel model) {
+        if (mModel != null) {
+            mModel.unregisterObserver(mObserver);
+        }
+        mModel = model;
+        if (mModel != null) {
+            mModel.registerObserver(mObserver);
+        }
+    }
+
+    private void init(Context context) {
+        mContext = context;
+
+        mPlayPauseStopImageContainer = inflate(context, R.layout.car_play_pause_stop_button_layout,
+                null);
+        mPlayPauseStopImageContainer.setOnClickListener(this::onPlayPauseStopClicked);
+        mPlayPauseStopImageView = mPlayPauseStopImageContainer.findViewById(R.id.play_pause_stop);
+        mSpinner = mPlayPauseStopImageContainer.findViewById(R.id.spinner);
+        mPlayPauseStopImageView.setAction(PlayPauseStopImageView.ACTION_DISABLED);
+        mPlayPauseStopImageView.setOnClickListener(this::onPlayPauseStopClicked);
+
+        mSkipPrevButton = createIconButton(mContext,
+                context.getDrawable(R.drawable.ic_skip_previous));
+        mSkipPrevButton.setVisibility(INVISIBLE);
+        mSkipPrevButton.setOnClickListener(v -> {
+            if (mModel != null) {
+                mModel.onSkipPreviews();
+            }
+        });
+        mSkipNextButton = createIconButton(mContext,
+                context.getDrawable(R.drawable.ic_skip_next));
+        mSkipNextButton.setVisibility(INVISIBLE);
+        mSkipNextButton.setOnClickListener(v -> {
+            if (mModel != null) {
+                mModel.onSkipNext();
+            }
+        });
+
+        setView(mPlayPauseStopImageContainer, ActionBar.SLOT_MAIN);
+        setView(mSkipPrevButton, ActionBar.SLOT_LEFT);
+        setView(mSkipNextButton, ActionBar.SLOT_RIGHT);
+    }
+
+    private ImageButton createIconButton(Context context, Drawable icon) {
+        ImageButton button = (ImageButton) inflate(context, R.layout.action_bar_button, null);
+        button.setImageDrawable(icon);
+        return button;
+    }
+
+    private void updateState() {
+        mPlayPauseStopImageView.setAction(convertMainAction(mModel.getMainAction()));
+        mSpinner.setVisibility(mModel.isBuffering() ? VISIBLE : INVISIBLE);
+        mSkipPrevButton.setVisibility(mModel.isSkipPreviewsEnabled() ? VISIBLE : INVISIBLE);
+        mSkipNextButton.setVisibility(mModel.isSkipNextEnabled() ? VISIBLE : INVISIBLE);
+    }
+
+    @PlayPauseStopImageView.Action
+    private int convertMainAction(@PlaybackModel.Action int action) {
+        switch (action) {
+            case PlaybackModel.ACTION_DISABLED:
+                return PlayPauseStopImageView.ACTION_DISABLED;
+            case PlaybackModel.ACTION_PLAY:
+                return PlayPauseStopImageView.ACTION_PLAY;
+            case PlaybackModel.ACTION_PAUSE:
+                return PlayPauseStopImageView.ACTION_PAUSE;
+            case PlaybackModel.ACTION_STOP:
+                return PlayPauseStopImageView.ACTION_STOP;
+        }
+        Log.w(TAG, "Unknown action: " + action);
+        return PlayPauseStopImageView.ACTION_DISABLED;
+    }
+
+    private void updateAccentColor() {
+        int color = mModel.getAccentColor();
+        int tintColor = ColorChecker.getTintColor(mContext, color);
+        mPlayPauseStopImageView.setPrimaryActionColor(color, tintColor);
+        mSpinner.setIndeterminateTintList(ColorStateList.valueOf(color));
+    }
+
+    private void updateCustomActions() {
+        List<CustomPlaybackAction> customActions = mModel.getCustomActions();
+
+        if (customActions.size() > mCustomActionButtons.size()) {
+            for (int i = mCustomActionButtons.size(); i < customActions.size(); i++) {
+                mCustomActionButtons.add(createIconButton(getContext(), null));
+            }
+            setViews(mCustomActionButtons.toArray(new View[mCustomActionButtons.size()]));
+            Log.i(TAG, "Increasing buttons array: " + customActions.size());
+        }
+        if (customActions.size() < mCustomActionButtons.size()) {
+            while (mCustomActionButtons.size() > customActions.size()) {
+                mCustomActionButtons.remove(mCustomActionButtons.size() - 1);
+            }
+            setViews(mCustomActionButtons.toArray(new View[mCustomActionButtons.size()]));
+            Log.i(TAG, "Decreasing buttons array: " + customActions.size());
+        }
+
+        for (int pos = 0; pos < mCustomActionButtons.size(); pos++) {
+            ImageButton button = mCustomActionButtons.get(pos);
+            if (customActions.size() > pos) {
+                button.setVisibility(VISIBLE);
+                button.setImageDrawable(customActions.get(pos).mIcon);
+            } else {
+                button.setVisibility(INVISIBLE);
+            }
+        }
+    }
+
+    private void onPlayPauseStopClicked(View view) {
+        if (mModel == null) {
+            return;
+        }
+        switch (mPlayPauseStopImageView.getAction()) {
+            case PlayPauseStopImageView.ACTION_PLAY:
+                mModel.onPlay();
+                break;
+            case PlayPauseStopImageView.ACTION_PAUSE:
+                mModel.onPause();
+                break;
+            case PlayPauseStopImageView.ACTION_STOP:
+                mModel.onStop();
+                break;
+            default:
+                Log.i(TAG, "Play/Pause/Stop clicked on invalid state");
+                break;
+        }
+    }
+}
diff --git a/car-media-common/src/com/android/car/media/common/PlaybackFragment.java b/car-media-common/src/com/android/car/media/common/PlaybackFragment.java
new file mode 100644
index 0000000..d56a186
--- /dev/null
+++ b/car-media-common/src/com/android/car/media/common/PlaybackFragment.java
@@ -0,0 +1,139 @@
+/*
+ * 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.common;
+
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+/**
+ * {@link Fragment} that can be used to display and control the currently playing media item.
+ * Its requires the android.Manifest.permission.MEDIA_CONTENT_CONTROL permission be held by the
+ * hosting application.
+ */
+public class PlaybackFragment extends Fragment {
+    private PlaybackModel mModel;
+    private ImageView mAlbumBackground;
+    private PlaybackControls mPlaybackControls;
+    private ImageView mAlbumArt;
+    private TextView mTitle;
+    private TextView mSubtitle;
+    private TextView mDescription;
+    private SeekBar mSeekbar;
+
+    private PlaybackModel.PlaybackObserver mObserver = new PlaybackModel.PlaybackObserver() {
+        @Override
+        public void onPlaybackStateChanged() {
+            updateState();
+        }
+
+        @Override
+        public void onSourceChanged() {
+            updateState();
+            updateMetadata();
+            updateAccentColor();
+        }
+
+        @Override
+        public void onMetadataChanged() {
+            updateMetadata();
+        }
+    };
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+            Bundle savedInstanceState) {
+        View view = inflater.inflate(R.layout.car_playback_fragment, container, false);
+        mModel = new PlaybackModel(getContext());
+        mModel.registerObserver(mObserver);
+        mAlbumBackground = view.findViewById(R.id.album_background);
+        mPlaybackControls = view.findViewById(R.id.playback_controls);
+        mPlaybackControls.setModel(mModel);
+        mAlbumArt = view.findViewById(R.id.album_art);
+        mTitle = view.findViewById(R.id.title);
+        mSubtitle = view.findViewById(R.id.subtitle);
+        mDescription = view.findViewById(R.id.description);
+        mSeekbar = view.findViewById(R.id.seek_bar);
+        return view;
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        mModel.start();
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        mModel.stop();
+    }
+
+    private void updateState() {
+        int maxProgress = mModel.getMaxProgress();
+        mSeekbar.setVisibility(maxProgress > 0 ? View.VISIBLE : View.INVISIBLE);
+        mSeekbar.setMax(maxProgress);
+        if (mModel.isPlaying()) {
+            mSeekbar.post(mSeekBarRunnable);
+        } else {
+            mSeekbar.removeCallbacks(mSeekBarRunnable);
+        }
+    }
+
+    private void updateMetadata() {
+        MediaItemMetadata metadata = mModel.getMetadata();
+        mTitle.setText(metadata != null ? metadata.mTitle : null);
+        mSubtitle.setText(metadata != null ? metadata.mSubtitle : null);
+        mDescription.setText(metadata != null ? metadata.mDescription : null);
+        Drawable art = metadata != null ? metadata.getAlbumArt() : null;
+        mAlbumArt.setImageDrawable(art);
+        mAlbumBackground.setImageDrawable(art);
+    }
+
+    private void updateAccentColor() {
+        int color = mModel.getAccentColor();
+        mSeekbar.getProgressDrawable().setColorFilter(color, PorterDuff.Mode.SRC_IN);
+    }
+
+    private static final long SEEK_BAR_UPDATE_TIME_INTERVAL_MS = 500;
+
+    private final Runnable mSeekBarRunnable = new Runnable() {
+        @Override
+        public void run() {
+            if (!mModel.isPlaying()) {
+                return;
+            }
+            mSeekbar.setProgress(mModel.getProgress());
+            mSeekbar.postDelayed(this, SEEK_BAR_UPDATE_TIME_INTERVAL_MS);
+        }
+    };
+}
diff --git a/car-media-common/src/com/android/car/media/common/PlaybackModel.java b/car-media-common/src/com/android/car/media/common/PlaybackModel.java
new file mode 100644
index 0000000..2e413a5
--- /dev/null
+++ b/car-media-common/src/com/android/car/media/common/PlaybackModel.java
@@ -0,0 +1,571 @@
+/*
+ * 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.common;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.media.MediaMetadata;
+import android.media.session.MediaController;
+import android.media.session.MediaSession;
+import android.media.session.MediaSessionManager;
+import android.media.session.PlaybackState;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+/**
+ * View-model for playback UI components. This abstractions provides a simplified view of
+ * {@link MediaSession} and {@link MediaSessionManager} data and events.
+ * <p>
+ * It automatically determines the foreground media app (the one that would normally
+ * receive playback events) and exposes metadata and events from such app, or when a different app
+ * becomes foreground.
+ * <p>
+ * This requires the android.Manifest.permission.MEDIA_CONTENT_CONTROL
+ * permission be held by the calling app.
+ */
+public class PlaybackModel {
+    private static final String TAG = "PlaybackModel";
+
+    private final MediaSessionManager mMediaSessionManager;
+    @Nullable
+    private MediaController mMediaController;
+    private Context mContext;
+    private List<PlaybackObserver> mObservers = new ArrayList<>();
+    private final MediaSessionUpdater mMediaSessionUpdater = new MediaSessionUpdater();
+
+    /**
+     * Temporary work-around to bug b/76017849.
+     * MediaSessionManager is not notifying media session priority changes.
+     * As a work-around we subscribe to playback state changes on all controllers to detect
+     * potential priority changes.
+     * This might cause a few unnecessary checks, but selecting the top-most controller is a
+     * cheap operation.
+     */
+    private class MediaSessionUpdater {
+        private Map<String, MediaController> mControllersByPackageName = new HashMap<>();
+
+        private MediaController.Callback mCallback = new MediaController.Callback() {
+            @Override
+            public void onPlaybackStateChanged(PlaybackState state) {
+                selectMediaController(mMediaSessionManager.getActiveSessions(null));
+            }
+        };
+
+        void setControllersByPackageName(List<MediaController> newControllers) {
+            Map<String, MediaController> newControllersMap = new HashMap<>();
+            for (MediaController newController : newControllers) {
+                if (!mControllersByPackageName.containsKey(newController.getPackageName())) {
+                    if (Log.isLoggable(TAG, Log.DEBUG)) {
+                        Log.d(TAG, "New controller detected: "
+                                + newController.getPackageName());
+                    }
+                    newController.registerCallback(mCallback);
+                } else {
+                    mControllersByPackageName.remove(newController.getPackageName());
+                }
+                newControllersMap.put(newController.getPackageName(), newController);
+            }
+            for (MediaController oldController : mControllersByPackageName.values()) {
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, "Removed controller detected: "
+                            + oldController.getPackageName());
+                }
+                oldController.unregisterCallback(mCallback);
+            }
+            mControllersByPackageName = newControllersMap;
+        }
+    }
+
+    /**
+     * An observer of this model
+     */
+    public abstract static class PlaybackObserver {
+        /**
+         * Called whenever the playback state of the current media item changes.
+         */
+        protected void onPlaybackStateChanged() {}
+
+        /**
+         * Called when the top source media app changes.
+         */
+        protected void onSourceChanged() {};
+
+        /**
+         * Called when the media item being played changes.
+         */
+        protected void onMetadataChanged() {};
+    }
+
+    private MediaController.Callback mCallback = new MediaController.Callback() {
+        @Override
+        public void onPlaybackStateChanged(PlaybackState state) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "onPlaybackStateChanged: " + state);
+            }
+            PlaybackModel.this.notify(PlaybackObserver::onPlaybackStateChanged);
+        }
+
+        @Override
+        public void onMetadataChanged(MediaMetadata metadata) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "onMetadataChanged: " + metadata);
+            }
+            PlaybackModel.this.notify(PlaybackObserver::onMetadataChanged);
+        }
+    };
+
+    private MediaSessionManager.OnActiveSessionsChangedListener mSessionChangeListener =
+            this::selectMediaController;
+
+    /**
+     * Creates a {@link PlaybackModel}. By default this instance is going to be inactive until
+     * {@link #start()} method is invoked.
+     */
+    public PlaybackModel(Context context) {
+        mContext = context;
+        mMediaSessionManager = mContext.getSystemService(MediaSessionManager.class);
+    }
+
+    private void selectMediaController(List<MediaController> controllers) {
+        changeMediaController(controllers != null && controllers.size() > 0 ? controllers.get(0) :
+                null);
+        mMediaSessionUpdater.setControllersByPackageName(controllers);
+    }
+
+    private void changeMediaController(MediaController mediaController) {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "New media controller: " + (mediaController != null
+                    ? mediaController.getPackageName() : null));
+        }
+        if (mediaController == mMediaController) {
+            // If no change, do nothing.
+            return;
+        }
+        if (mMediaController != null) {
+            mMediaController.unregisterCallback(mCallback);
+        }
+        mMediaController = mediaController;
+        if (mMediaController != null) {
+            mMediaController.registerCallback(mCallback);
+        }
+        notify(PlaybackObserver::onSourceChanged);
+    }
+
+    /**
+     * Starts following changes on the list of active media sources. If any changes happen, all
+     * observers registered through {@link #registerObserver(PlaybackObserver)} will be notified.
+     * <p>
+     * Calling this method might cause an immediate {@link PlaybackObserver#onSourceChanged()}
+     * event in case the current media source is different than the last known one.
+     */
+    public void start() {
+        mMediaSessionManager.addOnActiveSessionsChangedListener(mSessionChangeListener, null);
+        selectMediaController(mMediaSessionManager.getActiveSessions(null));
+    }
+
+    /**
+     * Stops following changes on the list of active media sources. This method could cause an
+     * immediate {@link PlaybackObserver#onSourceChanged()} event if a media source was already
+     * connected.
+     */
+    public void stop() {
+        mMediaSessionUpdater.setControllersByPackageName(new ArrayList<>());
+        mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionChangeListener);
+        changeMediaController(null);
+    }
+
+    private void notify(Consumer<PlaybackObserver> notification) {
+        for (PlaybackObserver observer : mObservers) {
+            notification.accept(observer);
+        }
+    }
+
+    /**
+     * @return the package name of the currently selected media source. Changes on this value will
+     * be notified through {@link PlaybackObserver#onSourceChanged()}
+     */
+    @Nullable
+    public String getPackageName() {
+        if (mMediaController == null) {
+            return null;
+        }
+        return mMediaController.getPackageName();
+    }
+
+    /**
+     * @return {@link Action} selected as the main action for the current media item, based on the
+     * current playback state and the available actions reported by the media source.
+     * Changes on this value will be notified through
+     * {@link PlaybackObserver#onPlaybackStateChanged()}
+     */
+    @Action
+    public int getMainAction() {
+        return getMainAction(mMediaController != null ? mMediaController.getPlaybackState() : null);
+    }
+
+    /**
+     * @return {@link MediaItemMetadata} of the currently selected media item in the media source.
+     * Changes on this value will be notified through {@link PlaybackObserver#onMetadataChanged()}
+     */
+    @Nullable
+    public MediaItemMetadata getMetadata() {
+        if (mMediaController == null) {
+            return null;
+        }
+        MediaMetadata metadata = mMediaController.getMetadata();
+        if (metadata == null) {
+            return null;
+        }
+        return new MediaItemMetadata(mContext, metadata);
+    }
+
+    /**
+     * @return an integer representing the maximum value for the progress bar corresponding on the
+     * current position in the media item, which can be obtained by calling {@link #getProgress()}.
+     * Changes on this value will be notified through {@link PlaybackObserver#onMetadataChanged()}
+     */
+    public int getMaxProgress() {
+        if (mMediaController == null || mMediaController.getMetadata() == null) {
+            return 0;
+        } else {
+            return (int) mMediaController.getMetadata()
+                    .getLong(MediaMetadata.METADATA_KEY_DURATION);
+        }
+    }
+
+    /**
+     * Sends a 'play' command to the media source
+     */
+    public void onPlay() {
+        if (mMediaController != null) {
+            mMediaController.getTransportControls().play();
+        }
+    }
+
+    /**
+     * Sends a 'skip previews' command to the media source
+     */
+    public void onSkipPreviews() {
+        if (mMediaController != null) {
+            mMediaController.getTransportControls().skipToPrevious();
+        }
+
+    }
+
+    /**
+     * Sends a 'skip next' command to the media source
+     */
+    public void onSkipNext() {
+        if (mMediaController != null) {
+            mMediaController.getTransportControls().skipToNext();
+        }
+    }
+
+    /**
+     * Sends a 'pause' command to the media source
+     */
+    public void onPause() {
+        if (mMediaController != null) {
+            mMediaController.getTransportControls().pause();
+        }
+    }
+
+    /**
+     * Sends a 'stop' command to the media source
+     */
+    public void onStop() {
+        if (mMediaController != null) {
+            mMediaController.getTransportControls().stop();
+        }
+    }
+
+    /**
+     * Sends a custom action to the media source
+     * @param action identifier of the custom action
+     * @param extras additional data to send to the media source.
+     */
+    public void onCustomAction(String action, Bundle extras) {
+        if (mMediaController != null) {
+            mMediaController.getTransportControls().sendCustomAction(action, extras);
+        }
+    }
+
+    /** Third-party defined application theme to use * */
+    private static final String THEME_META_DATA_NAME =
+            "com.google.android.gms.car.application.theme";
+
+    /**
+     * @return the accent color of the currently connected media source. Changes on this value will
+     * be notified through {@link PlaybackObserver#onSourceChanged()}
+     */
+    public int getAccentColor() {
+        if (mMediaController == null) {
+            return mContext.getResources().getColor(android.R.color.background_dark, null);
+        }
+        return getAccentColor(getPackageName());
+    }
+
+    private int getAccentColor(String packageName) {
+        int defaultColor = mContext.getResources().getColor(android.R.color.background_dark, null);
+        TypedArray ta = null;
+        try {
+            ApplicationInfo applicationInfo =
+                    mContext.getPackageManager().getApplicationInfo(packageName,
+                            PackageManager.GET_META_DATA);
+            // CharSequence title = applicationInfo.loadLabel(getContext().getPackageManager());
+            Context packageContext = mContext.createPackageContext(packageName, 0);
+            int appTheme = applicationInfo.metaData != null
+                    ? applicationInfo.metaData.getInt(THEME_META_DATA_NAME)
+                    : 0;
+            appTheme = appTheme == 0
+                    ? applicationInfo.theme
+                    : appTheme;
+            packageContext.setTheme(appTheme);
+            Resources.Theme theme = packageContext.getTheme();
+            ta = theme.obtainStyledAttributes(new int[] {
+                    android.R.attr.colorAccent
+            });
+            return ta.getColor(0, defaultColor);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.w(TAG, "Unable to obtain accent color from package: " + packageName);
+            return defaultColor;
+        } finally {
+            if (ta != null) {
+                ta.recycle();
+            }
+        }
+    }
+
+    /**
+     * Possible main actions.
+     */
+    @IntDef({ACTION_PLAY, ACTION_STOP, ACTION_PAUSE, ACTION_DISABLED})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Action {}
+
+    /** Main action is disabled. The source can't play media at this time */
+    public static final int ACTION_DISABLED = 0;
+    /** Start playing */
+    public static final int ACTION_PLAY = 1;
+    /** Stop playing */
+    public static final int ACTION_STOP = 2;
+    /** Pause playing */
+    public static final int ACTION_PAUSE = 3;
+
+    @Action
+    private static int getMainAction(PlaybackState state) {
+        if (state == null) {
+            return ACTION_DISABLED;
+        }
+        int stopAction = ((state.getActions() & PlaybackState.ACTION_PAUSE) != 0)
+                ? ACTION_PAUSE
+                : ACTION_STOP;
+        switch (state.getState()) {
+            case PlaybackState.STATE_PLAYING:
+            case PlaybackState.STATE_BUFFERING:
+            case PlaybackState.STATE_CONNECTING:
+            case PlaybackState.STATE_FAST_FORWARDING:
+            case PlaybackState.STATE_REWINDING:
+            case PlaybackState.STATE_SKIPPING_TO_NEXT:
+            case PlaybackState.STATE_SKIPPING_TO_PREVIOUS:
+            case PlaybackState.STATE_SKIPPING_TO_QUEUE_ITEM:
+                return stopAction;
+            case PlaybackState.STATE_STOPPED:
+            case PlaybackState.STATE_PAUSED:
+            case PlaybackState.STATE_NONE:
+                return ACTION_PLAY;
+            case PlaybackState.STATE_ERROR:
+                return ACTION_DISABLED;
+            default:
+                Log.w(TAG, String.format("Unknown PlaybackState: %d", state.getState()));
+                return ACTION_DISABLED;
+        }
+    }
+
+    /**
+     * @return the current playback progress. This is a value between 0 and
+     * {@link #getMaxProgress()}.
+     */
+    public int getProgress() {
+        if (mMediaController == null) {
+            return 0;
+        }
+        PlaybackState state = mMediaController.getPlaybackState();
+        if (state == null) {
+            return 0;
+        }
+        long timeDiff = SystemClock.elapsedRealtime() - state.getLastPositionUpdateTime();
+        float speed = state.getPlaybackSpeed();
+        if (state.getState() == PlaybackState.STATE_PAUSED
+                || state.getState() == PlaybackState.STATE_STOPPED) {
+            // This guards against apps who don't keep their playbackSpeed to spec (b/62375164)
+            speed = 0f;
+        }
+        long posDiff = (long) (timeDiff * speed);
+        return Math.min((int) (posDiff + state.getPosition()), getMaxProgress());
+    }
+
+    /**
+     * @return true if the current media source is playing a media item. Changes on this value
+     * would be notified through {@link PlaybackObserver#onPlaybackStateChanged()}
+     */
+    public boolean isPlaying() {
+        return mMediaController != null
+                && mMediaController.getPlaybackState() != null
+                && mMediaController.getPlaybackState().getState() == PlaybackState.STATE_PLAYING;
+    }
+
+    /**
+     * Registers an observer to be notified of media events.
+     */
+    public void registerObserver(PlaybackObserver observer) {
+        mObservers.add(observer);
+    }
+
+    /**
+     * Unregisters an observer previously registered using
+     * {@link #registerObserver(PlaybackObserver)}
+     */
+    public void unregisterObserver(PlaybackObserver observer) {
+        mObservers.remove(observer);
+    }
+
+    /**
+     * @return true if the media source supports skipping to next item. Changes on this value
+     * will be notified through {@link PlaybackObserver#onPlaybackStateChanged()}
+     */
+    public boolean isSkipNextEnabled() {
+        return mMediaController != null
+                && mMediaController.getPlaybackState() != null
+                && (mMediaController.getPlaybackState().getActions()
+                    & PlaybackState.ACTION_SKIP_TO_NEXT) != 0;
+    }
+
+    /**
+     * @return true if the media source supports skipping to previous item. Changes on this value
+     * will be notified through {@link PlaybackObserver#onPlaybackStateChanged()}
+     */
+    public boolean isSkipPreviewsEnabled() {
+        return mMediaController != null
+                && mMediaController.getPlaybackState() != null
+                && (mMediaController.getPlaybackState().getActions()
+                    & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0;
+    }
+
+    /**
+     * @return true if the media source is buffering. Changes on this value would be notified
+     * through {@link PlaybackObserver#onPlaybackStateChanged()}
+     */
+    public boolean isBuffering() {
+        return mMediaController != null
+                && mMediaController.getPlaybackState() != null
+                && mMediaController.getPlaybackState().getState() == PlaybackState.STATE_BUFFERING;
+    }
+
+    /**
+     * @return a human readable description of the error that cause the media source to be in a
+     * non-playable state, or null if there is no error. Changes on this value will be notified
+     * through {@link PlaybackObserver#onPlaybackStateChanged()}
+     */
+    @Nullable
+    public CharSequence getErrorMessage() {
+        return mMediaController != null && mMediaController.getPlaybackState() != null
+                ? mMediaController.getPlaybackState().getErrorMessage()
+                : null;
+    }
+
+    /**
+     * @return a sorted list of {@link MediaItemMetadata} corresponding to the queue of media items
+     * as reported by the media source. Changes on this value will be notified through
+     * {@link PlaybackObserver#onPlaybackStateChanged()}.
+     */
+    @NonNull
+    public List<MediaItemMetadata> getQueue() {
+        List<MediaSession.QueueItem> items = mMediaController.getQueue();
+        if (items != null) {
+            return items.stream()
+                    .map(item -> new MediaItemMetadata(mContext, item))
+                    .collect(Collectors.toList());
+        } else {
+            return new ArrayList<>();
+        }
+    }
+
+    /**
+     * @return true if the media queue is not empty. Detailed information can be obtained by
+     * calling to {@link #getQueue()}. Changes on this value will be notified through
+     * {@link PlaybackObserver#onPlaybackStateChanged()}.
+     */
+    public boolean hasQueue() {
+        List<MediaSession.QueueItem> items = mMediaController.getQueue();
+        return items != null && !items.isEmpty();
+    }
+
+    /**
+     * @return a sorted list of custom actions, as reported by the media source. Changes on this
+     * value will be notified through
+     * {@link PlaybackObserver#onPlaybackStateChanged()}.
+     */
+    public List<CustomPlaybackAction> getCustomActions() {
+        List<CustomPlaybackAction> actions = new ArrayList<>();
+        if (mMediaController == null || mMediaController.getPlaybackState() == null) {
+            return actions;
+        }
+        for (PlaybackState.CustomAction action : mMediaController.getPlaybackState()
+                .getCustomActions()) {
+            Resources resources = getResourcesForPackage(mMediaController.getPackageName());
+            if (resources == null) {
+                actions.add(null);
+            } else {
+                // the resources may be from another package. we need to update the configuration
+                // using the context from the activity so we get the drawable from the correct DPI
+                // bucket.
+                resources.updateConfiguration(mContext.getResources().getConfiguration(),
+                        mContext.getResources().getDisplayMetrics());
+                Drawable icon = resources.getDrawable(action.getIcon(), null);
+                actions.add(new CustomPlaybackAction(icon, action.getAction(), action.getExtras()));
+            }
+        }
+        return actions;
+    }
+
+    private Resources getResourcesForPackage(String packageName) {
+        try {
+            return mContext.getPackageManager().getResourcesForApplication(packageName);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(TAG, "Unable to get resources for " + packageName);
+            return null;
+        }
+    }
+}