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