New version of Auto media sample.
Bug: 17285064, Bug: 17255587, Bug: 17284035
Bug: 17277612, Bug: 17278055, Bug: 17278601
Bug: 17278054, Bug: 17169576, Bug: 16215981
Bug: 17529942
Change-Id: If1e5806dc0e4fa7da4b288f4e8adb50bb2400e4d
diff --git a/MusicDemo/.gitignore b/MusicDemo/.gitignore
new file mode 100644
index 0000000..963e828
--- /dev/null
+++ b/MusicDemo/.gitignore
@@ -0,0 +1,5 @@
+*.iml
+.gradle
+.idea
+build/
+local.properties
diff --git a/MusicDemo/README.txt b/MusicDemo/README.txt
new file mode 100644
index 0000000..97a506e
--- /dev/null
+++ b/MusicDemo/README.txt
@@ -0,0 +1,70 @@
+Android Automobile sample
+=========================
+
+
+Integration points
+------------------
+
+MusicService.java is the main entry point to the integration. It needs to:
+
+ - extend android.service.media.MediaBrowserService, implementing the media browsing related methods onGetRoot and onLoadChildren;
+ - start a new MediaSession and notify it's parent of the session's token (super.setSessionToken());
+ - set a callback on the MediaSession. The callback will receive all the user's actions, like play, pause, etc;
+ - handle all the actual music playing using any method your app prefers (for example, the Android MediaPlayer class)
+ - update info about the playing item and the playing queue using MediaSession (setMetadata, setPlaybackState, setQueue, setQueueTitle, etc)
+ - handle AudioManager focus change events and react appropriately (eg, pause when audio focus is lost)
+ - declare a meta-data tag in AndroidManifest.xml linking to a xml resource
+ with a <automotiveApp> root element. For a media app, this must include
+ an <uses name="media"/> element as a child.
+ For example, in AndroidManifest.xml:
+ <meta-data android:name="com.google.android.gms.car.application"
+ android:resource="@xml/automotive_app_desc"/>
+ And in res/values/automotive_app_desc.xml:
+ <?xml version="1.0" encoding="utf-8"?>
+ <automotiveApp>
+ <uses name="media"/>
+ </automotiveApp>
+
+ - be declared in AndroidManifest as an intent receiver for the action android.media.browse.MediaBrowserService:
+
+ <!-- Implement a service -->
+ <service
+ android:name=".service.MusicService"
+ android:exported="true"
+ >
+ <intent-filter>
+ <action android:name="android.media.browse.MediaBrowserService" />
+ </intent-filter>
+ </service>
+
+
+Optionally, you can listen to special intents that notify your app when a car is connected/disconnected. This may be useful if your app has special requirements when running on a car - for example, different media or ads. See CarPlugReceiver for more information.
+
+
+Customization
+-------------
+
+The car media app has only a few customization opportunities. You may:
+
+- Set the background color by using Android L primary color:
+ <style name="AppTheme" parent="android:Theme.Material">
+ <item name="android:colorPrimary">#ff0000</item>
+ </style>
+
+- Add custom actions in the state passed to setPlaybackState(state)
+
+- Handle custom actions in the MediaSession.Callback.onCustomAction
+
+
+
+Known issues:
+-------------
+
+- Sample: Resuming after pause makes the "Skip to previous" button disappear
+
+- Sample: playFromSearch creates a queue with search results, but then skip to next/previous don't work correctly because the queue is recreated without the search criteria
+
+- Emulator: running menu->search twice throws an exception.
+
+- Emulator: Under some circumstances, stop or onDestroy may never get called on MusicService and the MediaPlayer keeps locking some resources. Then, mediaPlayer.setDataSource on a new MediaPlayer object halts (probably) due to a deadlock. The workaround is to reboot the device.
+
diff --git a/MusicDemo/build.gradle b/MusicDemo/build.gradle
new file mode 100644
index 0000000..a0d7e19
--- /dev/null
+++ b/MusicDemo/build.gradle
@@ -0,0 +1,27 @@
+buildscript {
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:0.12.+'
+ }
+}
+
+apply plugin: 'android'
+
+android {
+ compileSdkVersion "android-L"
+ buildToolsVersion "21.0.0 rc1"
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_7
+ targetCompatibility JavaVersion.VERSION_1_7
+ }
+}
+
+
+dependencies {
+ compile 'com.android.support:support-v4:18.0.+'
+ compile 'com.android.support:appcompat-v7:18.0.+'
+}
diff --git a/MusicDemo/gradle.properties b/MusicDemo/gradle.properties
new file mode 100644
index 0000000..5d08ba7
--- /dev/null
+++ b/MusicDemo/gradle.properties
@@ -0,0 +1,18 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Settings specified in this file will override any Gradle settings
+# configured through the IDE.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx10248m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
\ No newline at end of file
diff --git a/MusicDemo/gradle/wrapper/gradle-wrapper.jar b/MusicDemo/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..8c0fb64
--- /dev/null
+++ b/MusicDemo/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/MusicDemo/gradle/wrapper/gradle-wrapper.properties b/MusicDemo/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..1e61d1f
--- /dev/null
+++ b/MusicDemo/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Apr 10 15:27:10 PDT 2013
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-all.zip
diff --git a/MusicDemo/gradlew b/MusicDemo/gradlew
new file mode 100755
index 0000000..91a7e26
--- /dev/null
+++ b/MusicDemo/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/MusicDemo/proguard-project.txt b/MusicDemo/proguard-project.txt
new file mode 100644
index 0000000..f2fe155
--- /dev/null
+++ b/MusicDemo/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/MusicDemo/src/main/AndroidManifest.xml b/MusicDemo/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..608dbc5
--- /dev/null
+++ b/MusicDemo/src/main/AndroidManifest.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 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.example.android.musicservicedemo"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
+
+ <uses-sdk
+ android:minSdkVersion="21"
+ android:targetSdkVersion="21" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@style/AppTheme">
+
+ <meta-data android:name="com.google.android.gms.car.application"
+ android:resource="@xml/automotive_app_desc"/>
+
+ <service
+ android:name=".MusicService"
+ android:exported="true"
+ >
+ <intent-filter>
+ <action android:name="android.media.browse.MediaBrowserService" />
+ </intent-filter>
+ </service>
+
+ <!-- (OPTIONAL) Use a broadcast receiver to listen to car connect/disconnect events -->
+ <receiver
+ android:name=".CarConnectionReceiver"
+ android:permission="com.google.android.gms.permission.CAR" >
+ <intent-filter>
+ <action android:name="com.google.android.gms.car.CONNECTED" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="com.google.android.gms.car.DISCONNECTED" />
+ </intent-filter>
+ </receiver>
+
+ </application>
+
+</manifest>
\ No newline at end of file
diff --git a/MusicDemo/src/main/java/com/example/android/musicservicedemo/CarConnectionReceiver.java b/MusicDemo/src/main/java/com/example/android/musicservicedemo/CarConnectionReceiver.java
new file mode 100644
index 0000000..de9ef4f
--- /dev/null
+++ b/MusicDemo/src/main/java/com/example/android/musicservicedemo/CarConnectionReceiver.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2014 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.example.android.musicservicedemo;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import com.example.android.musicservicedemo.utils.LogHelper;
+
+/**
+ * Broadcast receiver that gets notified whenever the device is connected to a compatible car.
+ */
+public class CarConnectionReceiver extends BroadcastReceiver {
+
+ private static final String TAG = "CarPlugReceiver";
+
+ private static final String CONNECTED_ACTION = "com.google.android.gms.car.CONNECTED";
+ private static final String DISCONNECTED_ACTION = "com.google.android.gms.car.DISCONNECTED";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (CONNECTED_ACTION.equals(intent.getAction())) {
+ LogHelper.i(TAG, "Device is connected to Android Auto");
+ } else if (DISCONNECTED_ACTION.equals(intent.getAction())) {
+ LogHelper.i(TAG, "Device is disconnected from Android Auto");
+ } else {
+ LogHelper.w(TAG, "Received unexpected broadcast intent. Intent action: ",
+ intent.getAction());
+ }
+ }
+}
diff --git a/MusicDemo/src/main/java/com/example/android/musicservicedemo/MediaNotification.java b/MusicDemo/src/main/java/com/example/android/musicservicedemo/MediaNotification.java
new file mode 100644
index 0000000..33d14c1
--- /dev/null
+++ b/MusicDemo/src/main/java/com/example/android/musicservicedemo/MediaNotification.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2014 Google Inc. All Rights Reserved.
+ *
+ * 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.example.android.musicservicedemo;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.Bitmap;
+import android.media.MediaDescription;
+import android.media.MediaMetadata;
+import android.media.session.MediaController;
+import android.media.session.MediaSession;
+import android.media.session.PlaybackState;
+import android.os.AsyncTask;
+import android.util.SparseArray;
+
+import com.example.android.musicservicedemo.utils.BitmapHelper;
+import com.example.android.musicservicedemo.utils.LogHelper;
+
+import java.io.IOException;
+
+/**
+ * Keeps track of a notification and updates it automatically for a given
+ * MediaSession. Maintaining a visible notification (usually) guarantees that the music service
+ * won't be killed during playback.
+ */
+public class MediaNotification extends BroadcastReceiver {
+ private static final String TAG = "MediaNotification";
+
+ private static final int NOTIFICATION_ID = 412;
+
+ public static final String ACTION_PAUSE = "com.example.android.musicservicedemo.pause";
+ public static final String ACTION_PLAY = "com.example.android.musicservicedemo.play";
+ public static final String ACTION_PREV = "com.example.android.musicservicedemo.prev";
+ public static final String ACTION_NEXT = "com.example.android.musicservicedemo.next";
+
+
+ private final MusicService mService;
+ private MediaSession.Token mSessionToken;
+ private MediaController mController;
+ private MediaController.TransportControls mTransportControls;
+ private final SparseArray<PendingIntent> mIntents = new SparseArray<PendingIntent>();
+
+ private PlaybackState mPlaybackState;
+ private MediaMetadata mMetadata;
+
+ private Notification.Builder mNotificationBuilder;
+ private NotificationManager mNotificationManager;
+ private Notification.Action mPlayPauseAction;
+
+ private String mCurrentAlbumArt;
+
+ private boolean mStarted = false;
+
+ public MediaNotification(MusicService service) {
+ mService = service;
+ updateSessionToken();
+
+ mNotificationManager = (NotificationManager) mService
+ .getSystemService(Context.NOTIFICATION_SERVICE);
+
+ String pkg = mService.getPackageName();
+ mIntents.put(android.R.drawable.ic_media_pause, PendingIntent.getBroadcast(mService, 100,
+ new Intent(ACTION_PAUSE).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT));
+ mIntents.put(android.R.drawable.ic_media_play, PendingIntent.getBroadcast(mService, 100,
+ new Intent(ACTION_PLAY).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT));
+ mIntents.put(android.R.drawable.ic_media_previous, PendingIntent.getBroadcast(mService, 100,
+ new Intent(ACTION_PREV).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT));
+ mIntents.put(android.R.drawable.ic_media_next, PendingIntent.getBroadcast(mService, 100,
+ new Intent(ACTION_NEXT).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT));
+ }
+
+ /**
+ * Posts the notification and starts tracking the session to keep it
+ * updated. The notification will automatically be removed if the session is
+ * destroyed before {@link #stopNotification} is called.
+ */
+ public void startNotification() {
+ if (!mStarted) {
+ mController.registerCallback(mCb);
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(ACTION_NEXT);
+ filter.addAction(ACTION_PAUSE);
+ filter.addAction(ACTION_PLAY);
+ filter.addAction(ACTION_PREV);
+ mService.registerReceiver(this, filter);
+
+ mMetadata = mController.getMetadata();
+ mPlaybackState = mController.getPlaybackState();
+
+ mStarted = true;
+ // The notification must be updated after setting started to true
+ updateNotificationMetadata();
+ }
+ }
+
+ /**
+ * Removes the notification and stops tracking the session. If the session
+ * was destroyed this has no effect.
+ */
+ public void stopNotification() {
+ mStarted = false;
+ mController.unregisterCallback(mCb);
+ try {
+ mService.unregisterReceiver(this);
+ } catch (IllegalArgumentException ex) {
+ // ignore if the receiver is not registered.
+ }
+ mService.stopForeground(true);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ LogHelper.d(TAG, "Received intent with action " + action);
+ if (ACTION_PAUSE.equals(action)) {
+ mTransportControls.pause();
+ } else if (ACTION_PLAY.equals(action)) {
+ mTransportControls.play();
+ } else if (ACTION_NEXT.equals(action)) {
+ mTransportControls.skipToNext();
+ } else if (ACTION_PREV.equals(action)) {
+ mTransportControls.skipToPrevious();
+ }
+ }
+
+ /**
+ * Update the state based on a change on the session token. Called either when
+ * we are running for the first time or when the media session owner has destroyed the session
+ * (see {@link android.media.session.MediaController.Callback#onSessionDestroyed()})
+ */
+ private void updateSessionToken() {
+ MediaSession.Token freshToken = mService.getSessionToken();
+ if (mSessionToken == null || !mSessionToken.equals(freshToken)) {
+ if (mController != null) {
+ mController.unregisterCallback(mCb);
+ }
+ mSessionToken = freshToken;
+ mController = new MediaController(mService, mSessionToken);
+ mTransportControls = mController.getTransportControls();
+ if (mStarted) {
+ mController.registerCallback(mCb);
+ }
+ }
+ }
+
+ private final MediaController.Callback mCb = new MediaController.Callback() {
+ @Override
+ public void onPlaybackStateChanged(PlaybackState state) {
+ mPlaybackState = state;
+ LogHelper.d(TAG, "Received new playback state", state);
+ updateNotificationPlaybackState();
+ }
+
+ @Override
+ public void onMetadataChanged(MediaMetadata metadata) {
+ mMetadata = metadata;
+ LogHelper.d(TAG, "Received new metadata ", metadata);
+ updateNotificationMetadata();
+ }
+
+ @Override
+ public void onSessionDestroyed() {
+ super.onSessionDestroyed();
+ LogHelper.d(TAG, "Session was destroyed, resetting to the new session token");
+ updateSessionToken();
+ }
+ };
+
+ private void updateNotificationMetadata() {
+ LogHelper.d(TAG, "updateNotificationMetadata. mMetadata=" + mMetadata);
+ if (mMetadata == null) {
+ return;
+ }
+
+ updatePlayPauseAction();
+
+ boolean firstRun = false;
+
+ if (mNotificationBuilder == null) {
+ firstRun = true;
+
+ mNotificationBuilder = new Notification.Builder(mService);
+
+ mNotificationBuilder
+ .addAction(android.R.drawable.ic_media_previous,
+ mService.getString(R.string.label_previous),
+ mIntents.get(android.R.drawable.ic_media_previous))
+ .addAction(mPlayPauseAction)
+ .addAction(android.R.drawable.ic_media_next,
+ mService.getString(R.string.label_next),
+ mIntents.get(android.R.drawable.ic_media_next))
+ .setStyle(new Notification.MediaStyle()
+ .setShowActionsInCompactView(1) // only show play/pause in compact view
+ .setMediaSession(mSessionToken))
+ .setColor(android.R.attr.colorPrimaryDark)
+ .setSmallIcon(R.drawable.ic_notification)
+ .setVisibility(Notification.VISIBILITY_PUBLIC)
+ .setUsesChronometer(true);
+ }
+
+ MediaDescription description = mMetadata.getDescription();
+ Bitmap art = description.getIconBitmap();
+ mNotificationBuilder
+ .setContentTitle(description.getTitle())
+ .setContentText(description.getSubtitle())
+ .setLargeIcon(art);
+
+ updateNotificationPlaybackState();
+
+ if (firstRun) {
+ mService.startForeground(NOTIFICATION_ID, mNotificationBuilder.build());
+ } else {
+ mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
+ }
+
+ if (art == null && description.getIconUri() != null) {
+ // This sample assumes the iconUri will be a valid URL formatted String, but
+ // it can actually be any valid Android Uri formatted String.
+ String albumUrl = description.getIconUri().toString();
+ if (mCurrentAlbumArt == null || !mCurrentAlbumArt.equals(albumUrl)) {
+ mCurrentAlbumArt = albumUrl;
+ // async fetch the album art icon
+ getBitmapFromURLAsync(albumUrl);
+ }
+ }
+ }
+
+ private void updatePlayPauseAction() {
+ LogHelper.d(TAG, "updatePlayPauseAction");
+ String playPauseLabel = "";
+ int playPauseIcon;
+ if (mPlaybackState.getState() == PlaybackState.STATE_PLAYING) {
+ playPauseLabel = mService.getString(R.string.label_pause);
+ playPauseIcon = android.R.drawable.ic_media_pause;
+ } else {
+ playPauseLabel = mService.getString(R.string.label_play);
+ playPauseIcon = android.R.drawable.ic_media_play;
+ }
+ if (mPlayPauseAction == null) {
+ mPlayPauseAction = new Notification.Action(playPauseIcon, playPauseLabel,
+ mIntents.get(playPauseIcon));
+ } else {
+ mPlayPauseAction.icon = playPauseIcon;
+ mPlayPauseAction.title = playPauseLabel;
+ mPlayPauseAction.actionIntent = mIntents.get(playPauseIcon);
+ }
+ }
+
+ private void updateNotificationPlaybackState() {
+ LogHelper.d(TAG, "updateNotificationPlaybackState. mPlaybackState=" + mPlaybackState);
+ if (mPlaybackState == null || !mStarted) {
+ LogHelper.d(TAG, "updateNotificationPlaybackState. cancelling notification!");
+ mService.stopForeground(true);
+ return;
+ }
+ if (mNotificationBuilder == null) {
+ LogHelper.d(TAG, "updateNotificationPlaybackState. there is no notificationBuilder. Ignoring request to update state!");
+ return;
+ }
+ if (mPlaybackState.getPosition() >= 0) {
+ LogHelper.d(TAG, "updateNotificationPlaybackState. updating playback position to ",
+ (System.currentTimeMillis() - mPlaybackState.getPosition()) / 1000, " seconds");
+ mNotificationBuilder
+ .setWhen(System.currentTimeMillis() - mPlaybackState.getPosition())
+ .setShowWhen(true)
+ .setUsesChronometer(true);
+ mNotificationBuilder.setShowWhen(true);
+ } else {
+ LogHelper.d(TAG, "updateNotificationPlaybackState. hiding playback position");
+ mNotificationBuilder
+ .setWhen(0)
+ .setShowWhen(false)
+ .setUsesChronometer(false);
+ }
+
+ updatePlayPauseAction();
+
+ mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
+ }
+
+ public void getBitmapFromURLAsync(final String source) {
+ LogHelper.d(TAG, "getBitmapFromURLAsync: starting asynctask to fetch ", source);
+ new AsyncTask() {
+ @Override
+ protected Object doInBackground(Object[] objects) {
+ try {
+ Bitmap bitmap = BitmapHelper.fetchAndRescaleBitmap(source,
+ BitmapHelper.MEDIA_ART_BIG_WIDTH, BitmapHelper.MEDIA_ART_BIG_HEIGHT);
+ if (mMetadata != null) {
+ MediaDescription currentDescription = mMetadata.getDescription();
+ // If the media is still the same, update the notification:
+ if (mNotificationBuilder != null &&
+ currentDescription.getIconUri().toString().equals(source)) {
+ LogHelper.d(TAG, "getBitmapFromURLAsync: set bitmap to ", source);
+ mCurrentAlbumArt = source;
+ mNotificationBuilder.setLargeIcon(bitmap);
+ mNotificationManager.notify(NOTIFICATION_ID,
+ mNotificationBuilder.build());
+ }
+ }
+ } catch (IOException e) {
+ LogHelper.e(TAG, e, "getBitmapFromURLAsync: " + source);
+ }
+ return null;
+ }
+ }.execute();
+ }
+
+}
diff --git a/MusicDemo/src/main/java/com/example/android/musicservicedemo/MusicService.java b/MusicDemo/src/main/java/com/example/android/musicservicedemo/MusicService.java
new file mode 100644
index 0000000..c2c535d
--- /dev/null
+++ b/MusicDemo/src/main/java/com/example/android/musicservicedemo/MusicService.java
@@ -0,0 +1,878 @@
+/*
+ * Copyright (C) 2014 Google Inc. All Rights Reserved.
+ *
+ * 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.example.android.musicservicedemo;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.MediaDescription;
+import android.media.MediaMetadata;
+import android.media.MediaPlayer;
+import android.media.MediaPlayer.OnCompletionListener;
+import android.media.MediaPlayer.OnErrorListener;
+import android.media.MediaPlayer.OnPreparedListener;
+import android.media.browse.MediaBrowser;
+import android.media.browse.MediaBrowser.MediaItem;
+import android.media.session.MediaSession;
+import android.media.session.PlaybackState;
+import android.net.Uri;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.WifiLock;
+import android.os.Bundle;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.service.media.MediaBrowserService;
+
+import com.example.android.musicservicedemo.model.MusicProvider;
+import com.example.android.musicservicedemo.utils.LogHelper;
+import com.example.android.musicservicedemo.utils.MediaIDHelper;
+import com.example.android.musicservicedemo.utils.QueueHelper;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.example.android.musicservicedemo.utils.MediaIDHelper.MEDIA_ID_MUSICS_BY_GENRE;
+import static com.example.android.musicservicedemo.utils.MediaIDHelper.MEDIA_ID_ROOT;
+import static com.example.android.musicservicedemo.utils.MediaIDHelper.createBrowseCategoryMediaID;
+import static com.example.android.musicservicedemo.utils.MediaIDHelper.extractBrowseCategoryFromMediaID;
+
+/**
+ * Main entry point for the Android Automobile integration. This class needs to:
+ *
+ * <ul>
+ *
+ * <li> Extend {@link android.service.media.MediaBrowserService}, implementing the media browsing
+ * related methods {@link android.service.media.MediaBrowserService#onGetRoot} and
+ * {@link android.service.media.MediaBrowserService#onLoadChildren};
+ * <li> Start a new {@link android.media.session.MediaSession} and notify its parent with the
+ * session's token {@link android.service.media.MediaBrowserService#setSessionToken};
+ *
+ * <li> Set a callback on the
+ * {@link android.media.session.MediaSession#setCallback(android.media.session.MediaSession.Callback)}.
+ * The callback will receive all the user's actions, like play, pause, etc;
+ *
+ * <li> Handle all the actual music playing using any method your app prefers (for example,
+ * {@link android.media.MediaPlayer})
+ *
+ * <li> Update playbackState, "now playing" metadata and queue, using MediaSession proper methods
+ * {@link android.media.session.MediaSession#setPlaybackState(android.media.session.PlaybackState)}
+ * {@link android.media.session.MediaSession#setMetadata(android.media.MediaMetadata)} and
+ * {@link android.media.session.MediaSession#setQueue(java.util.List)})
+ *
+ * <li> Be declared in AndroidManifest as an intent receiver for the action
+ * android.media.browse.MediaBrowserService
+ *
+ * <li> Declare a meta-data tag in AndroidManifest.xml linking to a xml resource
+ * with a <automotiveApp> root element. For a media app, this must include
+ * an <uses name="media"/> element as a child.
+ * For example, in AndroidManifest.xml:
+ * <meta-data android:name="com.google.android.gms.car.application"
+ * android:resource="@xml/automotive_app_desc"/>
+ * And in res/values/automotive_app_desc.xml:
+ * <automotiveApp>
+ * <uses name="media"/>
+ * </automotiveApp>
+ *
+ * </ul>
+
+ * <p>
+ * Customization:
+ *
+ * <li> Add custom actions in the state passed to setPlaybackState(state)
+ * <li> Handle custom actions in the MediaSession.Callback.onCustomAction
+ * <li> Use UI theme primaryColor to set the player color
+ *
+ * @see <a href="README.txt">README.txt</a> for more details.
+ *
+ */
+
+public class MusicService extends MediaBrowserService implements OnPreparedListener,
+ OnCompletionListener, OnErrorListener, AudioManager.OnAudioFocusChangeListener {
+
+ private static final String TAG = "MusicService";
+
+ // Action to thumbs up a media item
+ private static final String CUSTOM_ACTION_THUMBS_UP = "thumbs_up";
+
+ // The volume we set the media player to when we lose audio focus, but are
+ // allowed to reduce the volume instead of stopping playback.
+ public static final float VOLUME_DUCK = 0.2f;
+
+ // The volume we set the media player when we have audio focus.
+ public static final float VOLUME_NORMAL = 1.0f;
+ public static final String ANDROID_AUTO_PACKAGE_NAME = "com.google.android.projection.gearhead";
+ public static final String ANDROID_AUTO_EMULATOR_PACKAGE_NAME = "com.example.android.media";
+
+ // Music catalog manager
+ private MusicProvider mMusicProvider;
+
+ private MediaSession mSession;
+ private MediaPlayer mMediaPlayer;
+
+ // "Now playing" queue:
+ private List<MediaSession.QueueItem> mPlayingQueue;
+ private int mCurrentIndexOnQueue;
+
+ // Current local media player state
+ private int mState = PlaybackState.STATE_NONE;
+
+ // Wifi lock that we hold when streaming files from the internet, in order
+ // to prevent the device from shutting off the Wifi radio
+ private WifiLock mWifiLock;
+
+ private MediaNotification mMediaNotification;
+
+ enum AudioFocus {
+ NoFocusNoDuck, // we don't have audio focus, and can't duck
+ NoFocusCanDuck, // we don't have focus, but can play at a low volume
+ // ("ducking")
+ Focused // we have full audio focus
+ }
+
+ // Type of audio focus we have:
+ private AudioFocus mAudioFocus = AudioFocus.NoFocusNoDuck;
+ private AudioManager mAudioManager;
+
+ // Indicates if we should start playing immediately after we gain focus.
+ private boolean mPlayOnFocusGain;
+
+
+ /*
+ * (non-Javadoc)
+ * @see android.app.Service#onCreate()
+ */
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ LogHelper.d(TAG, "onCreate");
+
+ mPlayingQueue = new ArrayList<>();
+
+ // Create the Wifi lock (this does not acquire the lock, this just creates it)
+ mWifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE))
+ .createWifiLock(WifiManager.WIFI_MODE_FULL, "MusicDemo_lock");
+
+ // Create the music catalog metadata provider
+ mMusicProvider = new MusicProvider();
+ mMusicProvider.retrieveMedia(new MusicProvider.Callback() {
+ @Override
+ public void onMusicCatalogReady(boolean success) {
+ mState = success ? PlaybackState.STATE_STOPPED : PlaybackState.STATE_ERROR;
+ }
+ });
+
+ mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+
+ // Start a new MediaSession
+ mSession = new MediaSession(this, "MusicService");
+ setSessionToken(mSession.getSessionToken());
+ mSession.setCallback(new MediaSessionCallback());
+ updatePlaybackState(null);
+
+ mMediaNotification = new MediaNotification(this);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see android.app.Service#onDestroy()
+ */
+ @Override
+ public void onDestroy() {
+ LogHelper.d(TAG, "onDestroy");
+
+ // Service is being killed, so make sure we release our resources
+ handleStopRequest(null);
+
+ // In particular, always release the MediaSession to clean up resources
+ // and notify associated MediaController(s).
+ mSession.release();
+ }
+
+
+ // ********* MediaBrowserService methods:
+
+ @Override
+ public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
+ LogHelper.d(TAG, "OnGetRoot: clientPackageName=" + clientPackageName,
+ "; clientUid=" + clientUid + " ; rootHints=", rootHints);
+ // To ensure you are not allowing any arbitrary app to browse your app's contents, you
+ // need to check the origin:
+ if (!ANDROID_AUTO_PACKAGE_NAME.equals(clientPackageName) &&
+ !ANDROID_AUTO_EMULATOR_PACKAGE_NAME.equals(clientPackageName)) {
+ // If the request comes from an untrusted package, return null. No further calls will
+ // be made to other media browsing methods.
+ LogHelper.w(TAG, "OnGetRoot: IGNORING request from untrusted package " + clientPackageName);
+ return null;
+ }
+ return new BrowserRoot(MEDIA_ID_ROOT, null);
+ }
+
+ @Override
+ public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) {
+ if (!mMusicProvider.isInitialized()) {
+ // Use result.detach to allow calling result.sendResult from another thread:
+ result.detach();
+
+ mMusicProvider.retrieveMedia(new MusicProvider.Callback() {
+ @Override
+ public void onMusicCatalogReady(boolean success) {
+ if (success) {
+ loadChildrenImpl(parentMediaId, result);
+ } else {
+ updatePlaybackState(getString(R.string.error_no_metadata));
+ result.sendResult(new ArrayList<MediaItem>());
+ }
+ }
+ });
+
+ } else {
+ // If our music catalog is already loaded/cached, load them into result immediately
+ loadChildrenImpl(parentMediaId, result);
+ }
+ }
+
+ /**
+ * Actual implementation of onLoadChildren that assumes that MusicProvider is already
+ * initialized.
+ */
+ private void loadChildrenImpl(final String parentMediaId,
+ final Result<List<MediaBrowser.MediaItem>> result) {
+ LogHelper.d(TAG, "OnLoadChildren: parentMediaId=", parentMediaId);
+
+ List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>();
+
+ if (MEDIA_ID_ROOT.equals(parentMediaId)) {
+ LogHelper.d(TAG, "OnLoadChildren.ROOT");
+ mediaItems.add(new MediaBrowser.MediaItem(
+ new MediaDescription.Builder()
+ .setMediaId(MEDIA_ID_MUSICS_BY_GENRE)
+ .setTitle(getString(R.string.browse_genres))
+ .setIconUri(Uri.parse("android.resource://com.example.android.musicservicedemo/drawable/ic_by_genre"))
+ .setSubtitle(getString(R.string.browse_genre_subtitle))
+ .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE
+ ));
+
+ } else if (MEDIA_ID_MUSICS_BY_GENRE.equals(parentMediaId)) {
+ LogHelper.d(TAG, "OnLoadChildren.GENRES");
+ for (String genre: mMusicProvider.getGenres()) {
+ MediaBrowser.MediaItem item = new MediaBrowser.MediaItem(
+ new MediaDescription.Builder()
+ .setMediaId(createBrowseCategoryMediaID(MEDIA_ID_MUSICS_BY_GENRE, genre))
+ .setTitle(genre)
+ .setSubtitle(getString(R.string.browse_musics_by_genre_subtitle, genre))
+ .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE
+ );
+ mediaItems.add(item);
+ }
+
+ } else if (parentMediaId.startsWith(MEDIA_ID_MUSICS_BY_GENRE)) {
+ String genre = extractBrowseCategoryFromMediaID(parentMediaId)[1];
+ LogHelper.d(TAG, "OnLoadChildren.SONGS_BY_GENRE genre=", genre);
+ for (MediaMetadata track: mMusicProvider.getMusicsByGenre(genre)) {
+ // Since mediaMetadata fields are immutable, we need to create a copy, so we
+ // can set a hierarchy-aware mediaID. We will need to know the media hierarchy
+ // when we get a onPlayFromMusicID call, so we can create the proper queue based
+ // on where the music was selected from (by artist, by genre, random, etc)
+ String hierarchyAwareMediaID = MediaIDHelper.createTrackMediaID(
+ MEDIA_ID_MUSICS_BY_GENRE, genre, track);
+ MediaMetadata trackCopy = new MediaMetadata.Builder(track)
+ .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, hierarchyAwareMediaID)
+ .build();
+ MediaBrowser.MediaItem bItem = new MediaBrowser.MediaItem(
+ trackCopy.getDescription(), MediaItem.FLAG_PLAYABLE);
+ mediaItems.add(bItem);
+ }
+ } else {
+ LogHelper.w(TAG, "Skipping unmatched parentMediaId: ", parentMediaId);
+ }
+ result.sendResult(mediaItems);
+ }
+
+
+
+ // ********* MediaSession.Callback implementation:
+
+ private final class MediaSessionCallback extends MediaSession.Callback {
+ @Override
+ public void onPlay() {
+ LogHelper.d(TAG, "play");
+
+ if (mPlayingQueue == null || mPlayingQueue.isEmpty()) {
+ mPlayingQueue = QueueHelper.getRandomQueue(mMusicProvider);
+ mSession.setQueue(mPlayingQueue);
+ mSession.setQueueTitle(getString(R.string.random_queue_title));
+ // start playing from the beginning of the queue
+ mCurrentIndexOnQueue = 0;
+ }
+
+ if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
+ handlePlayRequest();
+ }
+ }
+
+ @Override
+ public void onSkipToQueueItem(long queueId) {
+ LogHelper.d(TAG, "OnSkipToQueueItem:" + queueId);
+ if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
+
+ // set the current index on queue from the music Id:
+ mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue(mPlayingQueue, queueId);
+
+ // play the music
+ handlePlayRequest();
+ }
+ }
+
+ @Override
+ public void onPlayFromMediaId(String mediaId, Bundle extras) {
+ LogHelper.d(TAG, "playFromMediaId mediaId:", mediaId, " extras=", extras);
+
+ // The mediaId used here is not the unique musicId. This one comes from the
+ // MediaBrowser, and is actually a "hierarchy-aware mediaID": a concatenation of
+ // the hierarchy in MediaBrowser and the actual unique musicID. This is necessary
+ // so we can build the correct playing queue, based on where the track was
+ // selected from.
+ mPlayingQueue = QueueHelper.getPlayingQueue(mediaId, mMusicProvider);
+ mSession.setQueue(mPlayingQueue);
+ String queueTitle = getString(R.string.browse_musics_by_genre_subtitle,
+ MediaIDHelper.extractBrowseCategoryValueFromMediaID(mediaId));
+ mSession.setQueueTitle(queueTitle);
+
+ if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
+ String uniqueMusicID = MediaIDHelper.extractMusicIDFromMediaID(mediaId);
+ // set the current index on queue from the music Id:
+ mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue(
+ mPlayingQueue, uniqueMusicID);
+
+ // play the music
+ handlePlayRequest();
+ }
+ }
+
+ @Override
+ public void onPause() {
+ LogHelper.d(TAG, "pause. current state=" + mState);
+ handlePauseRequest();
+ }
+
+ @Override
+ public void onStop() {
+ LogHelper.d(TAG, "stop. current state=" + mState);
+ handleStopRequest(null);
+ }
+
+ @Override
+ public void onSkipToNext() {
+ LogHelper.d(TAG, "skipToNext");
+ mCurrentIndexOnQueue++;
+ if (mPlayingQueue != null && mCurrentIndexOnQueue >= mPlayingQueue.size()) {
+ mCurrentIndexOnQueue = 0;
+ }
+ if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
+ mState = PlaybackState.STATE_PLAYING;
+ handlePlayRequest();
+ } else {
+ LogHelper.e(TAG, "skipToNext: cannot skip to next. next Index=" +
+ mCurrentIndexOnQueue + " queue length=" +
+ (mPlayingQueue == null ? "null" : mPlayingQueue.size()));
+ handleStopRequest("Cannot skip");
+ }
+ }
+
+ @Override
+ public void onSkipToPrevious() {
+ LogHelper.d(TAG, "skipToPrevious");
+
+ mCurrentIndexOnQueue--;
+ if (mPlayingQueue != null && mCurrentIndexOnQueue < 0) {
+ // This sample's behavior: skipping to previous when in first song restarts the
+ // first song.
+ mCurrentIndexOnQueue = 0;
+ }
+ if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
+ mState = PlaybackState.STATE_PLAYING;
+ handlePlayRequest();
+ } else {
+ LogHelper.e(TAG, "skipToPrevious: cannot skip to previous. previous Index=" +
+ mCurrentIndexOnQueue + " queue length=" +
+ (mPlayingQueue == null ? "null" : mPlayingQueue.size()));
+ handleStopRequest("Cannot skip");
+ }
+ }
+
+ @Override
+ public void onCustomAction(String action, Bundle extras) {
+ if (CUSTOM_ACTION_THUMBS_UP.equals(action)) {
+ LogHelper.i(TAG, "onCustomAction: favorite for current track");
+ MediaMetadata track = getCurrentPlayingMusic();
+ if (track != null) {
+ String mediaId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
+ mMusicProvider.setFavorite(mediaId, !mMusicProvider.isFavorite(mediaId));
+ }
+ updatePlaybackState(null);
+ } else {
+ LogHelper.e(TAG, "Unsupported action: ", action);
+ }
+
+ }
+
+ @Override
+ public void onPlayFromSearch(String query, Bundle extras) {
+ LogHelper.d(TAG, "playFromSearch query=", query);
+
+ mPlayingQueue = QueueHelper.getPlayingQueueFromSearch(query, mMusicProvider);
+ LogHelper.d(TAG, "playFromSearch playqueue.length=" + mPlayingQueue.size());
+ mSession.setQueue(mPlayingQueue);
+
+ if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
+
+ // start playing from the beginning of the queue
+ mCurrentIndexOnQueue = 0;
+
+ handlePlayRequest();
+ }
+ }
+ }
+
+
+
+ // ********* MediaPlayer listeners:
+
+ /*
+ * Called when media player is done playing current song.
+ * @see android.media.MediaPlayer.OnCompletionListener
+ */
+ @Override
+ public void onCompletion(MediaPlayer player) {
+ LogHelper.d(TAG, "onCompletion from MediaPlayer");
+ // The media player finished playing the current song, so we go ahead
+ // and start the next.
+ if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
+ // In this sample, we restart the playing queue when it gets to the end:
+ mCurrentIndexOnQueue++;
+ if (mCurrentIndexOnQueue >= mPlayingQueue.size()) {
+ mCurrentIndexOnQueue = 0;
+ }
+ handlePlayRequest();
+ } else {
+ // If there is nothing to play, we stop and release the resources:
+ handleStopRequest(null);
+ }
+ }
+
+ /*
+ * Called when media player is done preparing.
+ * @see android.media.MediaPlayer.OnPreparedListener
+ */
+ @Override
+ public void onPrepared(MediaPlayer player) {
+ LogHelper.d(TAG, "onPrepared from MediaPlayer");
+ // The media player is done preparing. That means we can start playing if we
+ // have audio focus.
+ configMediaPlayerState();
+ }
+
+ /**
+ * Called when there's an error playing media. When this happens, the media
+ * player goes to the Error state. We warn the user about the error and
+ * reset the media player.
+ *
+ * @see android.media.MediaPlayer.OnErrorListener
+ */
+ @Override
+ public boolean onError(MediaPlayer mp, int what, int extra) {
+ LogHelper.e(TAG, "Media player error: what=" + what + ", extra=" + extra);
+ handleStopRequest("MediaPlayer error " + what + " (" + extra + ")");
+ return true; // true indicates we handled the error
+ }
+
+
+
+
+ // ********* OnAudioFocusChangeListener listener:
+
+
+ /**
+ * Called by AudioManager on audio focus changes.
+ */
+ @Override
+ public void onAudioFocusChange(int focusChange) {
+ LogHelper.d(TAG, "onAudioFocusChange. focusChange=" + focusChange);
+ if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
+ // We have gained focus:
+ mAudioFocus = AudioFocus.Focused;
+
+ } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS ||
+ focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT ||
+ focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
+ // We have lost focus. If we can duck (low playback volume), we can keep playing.
+ // Otherwise, we need to pause the playback.
+ boolean canDuck = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK;
+ mAudioFocus = canDuck ? AudioFocus.NoFocusCanDuck : AudioFocus.NoFocusNoDuck;
+
+ // If we are playing, we need to reset media player by calling configMediaPlayerState
+ // with mAudioFocus properly set.
+ if (mState == PlaybackState.STATE_PLAYING && !canDuck) {
+ // If we don't have audio focus and can't duck, we save the information that
+ // we were playing, so that we can resume playback once we get the focus back.
+ mPlayOnFocusGain = true;
+ }
+ } else {
+ LogHelper.e(TAG, "onAudioFocusChange: Ignoring unsupported focusChange: " + focusChange);
+ }
+
+ configMediaPlayerState();
+ }
+
+
+
+ // ********* private methods:
+
+ /**
+ * Handle a request to play music
+ */
+ private void handlePlayRequest() {
+ LogHelper.d(TAG, "handlePlayRequest: mState=" + mState);
+
+ mPlayOnFocusGain = true;
+ tryToGetAudioFocus();
+
+ if (!mSession.isActive()) {
+ mSession.setActive(true);
+ }
+
+ // actually play the song
+ if (mState == PlaybackState.STATE_PAUSED) {
+ // If we're paused, just continue playback and restore the
+ // 'foreground service' state.
+ configMediaPlayerState();
+ } else {
+ // If we're stopped or playing a song,
+ // just go ahead to the new song and (re)start playing
+ playCurrentSong();
+ }
+ }
+
+
+ /**
+ * Handle a request to pause music
+ */
+ private void handlePauseRequest() {
+ LogHelper.d(TAG, "handlePauseRequest: mState=" + mState);
+
+ if (mState == PlaybackState.STATE_PLAYING) {
+ // Pause media player and cancel the 'foreground service' state.
+ mState = PlaybackState.STATE_PAUSED;
+ if (mMediaPlayer.isPlaying()) {
+ mMediaPlayer.pause();
+ }
+ // while paused, retain the MediaPlayer but give up audio focus
+ relaxResources(false);
+ giveUpAudioFocus();
+ }
+ updatePlaybackState(null);
+ }
+
+ /**
+ * Handle a request to stop music
+ */
+ private void handleStopRequest(String withError) {
+ LogHelper.d(TAG, "handleStopRequest: mState=" + mState + " error=", withError);
+ mState = PlaybackState.STATE_STOPPED;
+
+ // let go of all resources...
+ relaxResources(true);
+ giveUpAudioFocus();
+ updatePlaybackState(withError);
+
+ mMediaNotification.stopNotification();
+
+ // service is no longer necessary. Will be started again if needed.
+ stopSelf();
+ }
+
+ /**
+ * Releases resources used by the service for playback. This includes the
+ * "foreground service" status, the wake locks and possibly the MediaPlayer.
+ *
+ * @param releaseMediaPlayer Indicates whether the Media Player should also
+ * be released or not
+ */
+ private void relaxResources(boolean releaseMediaPlayer) {
+ LogHelper.d(TAG, "relaxResources. releaseMediaPlayer=" + releaseMediaPlayer);
+ // stop being a foreground service
+ stopForeground(true);
+
+ // stop and release the Media Player, if it's available
+ if (releaseMediaPlayer && mMediaPlayer != null) {
+ mMediaPlayer.reset();
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ }
+
+ // we can also release the Wifi lock, if we're holding it
+ if (mWifiLock.isHeld()) {
+ mWifiLock.release();
+ }
+ }
+
+ /**
+ * Reconfigures MediaPlayer according to audio focus settings and
+ * starts/restarts it. This method starts/restarts the MediaPlayer
+ * respecting the current audio focus state. So if we have focus, it will
+ * play normally; if we don't have focus, it will either leave the
+ * MediaPlayer paused or set it to a low volume, depending on what is
+ * allowed by the current focus settings. This method assumes mPlayer !=
+ * null, so if you are calling it, you have to do so from a context where
+ * you are sure this is the case.
+ */
+ private void configMediaPlayerState() {
+ LogHelper.d(TAG, "configAndStartMediaPlayer. mAudioFocus=" + mAudioFocus);
+ if (mAudioFocus == AudioFocus.NoFocusNoDuck) {
+ // If we don't have audio focus and can't duck, we have to pause,
+ if (mState == PlaybackState.STATE_PLAYING) {
+ handlePauseRequest();
+ }
+ } else { // we have audio focus:
+ if (mAudioFocus == AudioFocus.NoFocusCanDuck) {
+ mMediaPlayer.setVolume(VOLUME_DUCK, VOLUME_DUCK); // we'll be relatively quiet
+ } else {
+ mMediaPlayer.setVolume(VOLUME_NORMAL, VOLUME_NORMAL); // we can be loud again
+ }
+ // If we were playing when we lost focus, we need to resume playing.
+ if (mPlayOnFocusGain) {
+ if (!mMediaPlayer.isPlaying()) {
+ LogHelper.d(TAG, "configAndStartMediaPlayer startMediaPlayer.");
+ mMediaPlayer.start();
+ }
+ mPlayOnFocusGain = false;
+ mState = PlaybackState.STATE_PLAYING;
+ }
+ }
+ updatePlaybackState(null);
+ }
+
+ /**
+ * Makes sure the media player exists and has been reset. This will create
+ * the media player if needed, or reset the existing media player if one
+ * already exists.
+ */
+ private void createMediaPlayerIfNeeded() {
+ LogHelper.d(TAG, "createMediaPlayerIfNeeded. needed? " + (mMediaPlayer==null));
+ if (mMediaPlayer == null) {
+ mMediaPlayer = new MediaPlayer();
+
+ // Make sure the media player will acquire a wake-lock while
+ // playing. If we don't do that, the CPU might go to sleep while the
+ // song is playing, causing playback to stop.
+ mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
+
+ // we want the media player to notify us when it's ready preparing,
+ // and when it's done playing:
+ mMediaPlayer.setOnPreparedListener(this);
+ mMediaPlayer.setOnCompletionListener(this);
+ mMediaPlayer.setOnErrorListener(this);
+ } else {
+ mMediaPlayer.reset();
+ }
+ }
+
+ /**
+ * Starts playing the current song in the playing queue.
+ */
+ void playCurrentSong() {
+ MediaMetadata track = getCurrentPlayingMusic();
+ if (track == null) {
+ LogHelper.e(TAG, "playSong: ignoring request to play next song, because cannot" +
+ " find it." +
+ " currentIndex=" + mCurrentIndexOnQueue +
+ " playQueue.size=" + (mPlayingQueue==null?"null": mPlayingQueue.size()));
+ return;
+ }
+ String source = track.getString(MusicProvider.CUSTOM_METADATA_TRACK_SOURCE);
+ LogHelper.d(TAG, "playSong: current (" + mCurrentIndexOnQueue + ") in playingQueue. " +
+ " musicId=" + track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID) +
+ " source=" + source);
+
+ mState = PlaybackState.STATE_STOPPED;
+ relaxResources(false); // release everything except MediaPlayer
+
+ try {
+ createMediaPlayerIfNeeded();
+
+ mState = PlaybackState.STATE_BUFFERING;
+
+ mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+ LogHelper.d(TAG, "****** playCurrentSong: about to call setDataSource. If no" +
+ "'finished' log message shows up right after this, it's because the media " +
+ "player is stuck in a deadlock. This is a known issue. In the meantime, you " +
+ "will need to restart the device.");
+ try {
+ mMediaPlayer.setDataSource(source);
+ } finally {
+ LogHelper.d(TAG, "****** playCurrentSong: setDataSource finished, no deadlock :-)");
+ }
+
+ // Starts preparing the media player in the background. When
+ // it's done, it will call our OnPreparedListener (that is,
+ // the onPrepared() method on this class, since we set the
+ // listener to 'this'). Until the media player is prepared,
+ // we *cannot* call start() on it!
+ mMediaPlayer.prepareAsync();
+
+ // If we are streaming from the internet, we want to hold a
+ // Wifi lock, which prevents the Wifi radio from going to
+ // sleep while the song is playing.
+ mWifiLock.acquire();
+
+ updatePlaybackState(null);
+ updateMetadata();
+
+ } catch (IOException ex) {
+ LogHelper.e(TAG, ex, "IOException playing song");
+ updatePlaybackState(ex.getMessage());
+ }
+ }
+
+
+
+ private void updateMetadata() {
+ if (!QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
+ LogHelper.e(TAG, "Can't retrieve current metadata.");
+ mState = PlaybackState.STATE_ERROR;
+ updatePlaybackState(getResources().getString(R.string.error_no_metadata));
+ return;
+ }
+ MediaSession.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue);
+ String mediaId = queueItem.getDescription().getMediaId();
+ MediaMetadata track = mMusicProvider.getMusic(mediaId);
+ String trackId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
+ if (!mediaId.equals(trackId)) {
+ throw new IllegalStateException("track ID (" + trackId + ") " +
+ "should match mediaId (" + mediaId + ")");
+ }
+ LogHelper.d(TAG, "Updating metadata for MusicID= " + mediaId);
+ mSession.setMetadata(track);
+ }
+
+
+ /**
+ * Update the current media player state, optionally showing an error message.
+ *
+ * @param error if not null, error message to present to the user.
+ *
+ */
+ private void updatePlaybackState(String error) {
+ LogHelper.d(TAG, "updatePlaybackState, setting session playback state to " + mState);
+ long position = PlaybackState.PLAYBACK_POSITION_UNKNOWN;
+ if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
+ position = mMediaPlayer.getCurrentPosition();
+ }
+ PlaybackState.Builder stateBuilder = new PlaybackState.Builder()
+ .setActions(getAvailableActions());
+
+ setCustomAction(stateBuilder);
+
+ // If there is an error message, send it to the playback state:
+ if (error != null) {
+ // Error states are really only supposed to be used for errors that cause playback to
+ // stop unexpectedly and persist until the user takes action to fix it.
+ stateBuilder.setErrorMessage(error);
+ mState = PlaybackState.STATE_ERROR;
+ }
+ stateBuilder.setState(mState, position, 1.0f, SystemClock.elapsedRealtime());
+
+ mSession.setPlaybackState(stateBuilder.build());
+
+ if (mState == PlaybackState.STATE_PLAYING || mState == PlaybackState.STATE_PAUSED) {
+ mMediaNotification.startNotification();
+ }
+ }
+
+ private void setCustomAction(PlaybackState.Builder stateBuilder) {
+ MediaMetadata currentMusic = getCurrentPlayingMusic();
+ if (currentMusic != null) {
+ // Set appropriate "Favorite" icon on Custom action:
+ String mediaId = currentMusic.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
+ int favoriteIcon = android.R.drawable.btn_star_big_off;
+ if (mMusicProvider.isFavorite(mediaId)) {
+ favoriteIcon = android.R.drawable.btn_star_big_on;
+ }
+ LogHelper.d(TAG, "updatePlaybackState, setting Favorite custom action of music ",
+ mediaId, " current favorite=", mMusicProvider.isFavorite(mediaId));
+ stateBuilder.addCustomAction(CUSTOM_ACTION_THUMBS_UP, getString(R.string.favorite),
+ favoriteIcon);
+ }
+ }
+
+ private long getAvailableActions() {
+ long actions = PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID |
+ PlaybackState.ACTION_PLAY_FROM_SEARCH;
+ if (mPlayingQueue == null || mPlayingQueue.isEmpty()) {
+ return actions;
+ }
+ if (mState == PlaybackState.STATE_PLAYING) {
+ actions |= PlaybackState.ACTION_PAUSE;
+ }
+ if (mCurrentIndexOnQueue > 0) {
+ actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS;
+ }
+ if (mCurrentIndexOnQueue < mPlayingQueue.size() - 1) {
+ actions |= PlaybackState.ACTION_SKIP_TO_NEXT;
+ }
+ return actions;
+ }
+
+ private MediaMetadata getCurrentPlayingMusic() {
+ if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
+ MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue);
+ if (item != null) {
+ LogHelper.d(TAG, "getCurrentPlayingMusic for musicId=",
+ item.getDescription().getMediaId());
+ return mMusicProvider.getMusic(item.getDescription().getMediaId());
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Try to get the system audio focus.
+ */
+ void tryToGetAudioFocus() {
+ LogHelper.d(TAG, "tryToGetAudioFocus");
+ if (mAudioFocus != AudioFocus.Focused) {
+ int result = mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
+ AudioManager.AUDIOFOCUS_GAIN);
+ if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+ mAudioFocus = AudioFocus.Focused;
+ }
+ }
+
+ }
+
+ /**
+ * Give up the audio focus.
+ */
+ void giveUpAudioFocus() {
+ LogHelper.d(TAG, "giveUpAudioFocus");
+ if (mAudioFocus == AudioFocus.Focused) {
+ if (mAudioManager.abandonAudioFocus(this) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+ mAudioFocus = AudioFocus.NoFocusNoDuck;
+ }
+ }
+ }
+}
diff --git a/MusicDemo/src/main/java/com/example/android/musicservicedemo/model/MusicProvider.java b/MusicDemo/src/main/java/com/example/android/musicservicedemo/model/MusicProvider.java
new file mode 100644
index 0000000..dd89c2d
--- /dev/null
+++ b/MusicDemo/src/main/java/com/example/android/musicservicedemo/model/MusicProvider.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2014 Google Inc. All Rights Reserved.
+ *
+ * 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.example.android.musicservicedemo.model;
+
+import android.media.MediaMetadata;
+import android.os.AsyncTask;
+
+import com.example.android.musicservicedemo.utils.LogHelper;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Utility class to get a list of MusicTrack's based on a server-side JSON
+ * configuration.
+ */
+public class MusicProvider {
+
+ private static final String TAG = "MusicProvider";
+
+ private static final String CATALOG_URL = "http://storage.googleapis.com/automotive-media/music.json";
+
+ public static final String CUSTOM_METADATA_TRACK_SOURCE = "__SOURCE__";
+
+ private static String JSON_MUSIC = "music";
+ private static String JSON_TITLE = "title";
+ private static String JSON_ALBUM = "album";
+ private static String JSON_ARTIST = "artist";
+ private static String JSON_GENRE = "genre";
+ private static String JSON_SOURCE = "source";
+ private static String JSON_IMAGE = "image";
+ private static String JSON_TRACK_NUMBER = "trackNumber";
+ private static String JSON_TOTAL_TRACK_COUNT = "totalTrackCount";
+ private static String JSON_DURATION = "duration";
+
+ private final ReentrantLock initializationLock = new ReentrantLock();
+
+ // Categorized caches for music track data:
+ private final HashMap<String, List<MediaMetadata>> mMusicListByGenre;
+ private final HashMap<String, MediaMetadata> mMusicListById;
+
+ private final HashSet<String> mFavoriteTracks;
+
+ enum State {
+ NON_INITIALIZED, INITIALIZING, INITIALIZED;
+ }
+
+ private State mCurrentState = State.NON_INITIALIZED;
+
+
+ public interface Callback {
+ void onMusicCatalogReady(boolean success);
+ }
+
+ public MusicProvider() {
+ mMusicListByGenre = new HashMap<>();
+ mMusicListById = new HashMap<>();
+ mFavoriteTracks = new HashSet<>();
+ }
+
+ /**
+ * Get an iterator over the list of genres
+ *
+ * @return
+ */
+ public Iterable<String> getGenres() {
+ if (mCurrentState != State.INITIALIZED) {
+ return new ArrayList<String>(0);
+ }
+ return mMusicListByGenre.keySet();
+ }
+
+ /**
+ * Get music tracks of the given genre
+ *
+ * @return
+ */
+ public Iterable<MediaMetadata> getMusicsByGenre(String genre) {
+ if (mCurrentState != State.INITIALIZED || !mMusicListByGenre.containsKey(genre)) {
+ return new ArrayList<MediaMetadata>();
+ }
+ return mMusicListByGenre.get(genre);
+ }
+
+ /**
+ * Very basic implementation of a search that filter music tracks which title containing
+ * the given query.
+ *
+ * @return
+ */
+ public Iterable<MediaMetadata> searchMusics(String titleQuery) {
+ ArrayList<MediaMetadata> result = new ArrayList<>();
+ if (mCurrentState != State.INITIALIZED) {
+ return result;
+ }
+ titleQuery = titleQuery.toLowerCase();
+ for (MediaMetadata track: mMusicListById.values()) {
+ if (track.getString(MediaMetadata.METADATA_KEY_TITLE).toLowerCase()
+ .contains(titleQuery)) {
+ result.add(track);
+ }
+ }
+ return result;
+ }
+
+ public MediaMetadata getMusic(String mediaId) {
+ return mMusicListById.get(mediaId);
+ }
+
+ public void setFavorite(String mediaId, boolean favorite) {
+ if (favorite) {
+ mFavoriteTracks.add(mediaId);
+ } else {
+ mFavoriteTracks.remove(mediaId);
+ }
+ }
+
+ public boolean isFavorite(String musicId) {
+ return mFavoriteTracks.contains(musicId);
+ }
+
+ public boolean isInitialized() {
+ return mCurrentState == State.INITIALIZED;
+ }
+
+ /**
+ * Get the list of music tracks from a server and caches the track information
+ * for future reference, keying tracks by mediaId and grouping by genre.
+ *
+ * @return
+ */
+ public void retrieveMedia(final Callback callback) {
+
+ if (mCurrentState == State.INITIALIZED) {
+ // Nothing to do, execute callback immediately
+ callback.onMusicCatalogReady(true);
+ return;
+ }
+
+ // Asynchronously load the music catalog in a separate thread
+ new AsyncTask() {
+ @Override
+ protected Object doInBackground(Object[] objects) {
+ retrieveMediaAsync(callback);
+ return null;
+ }
+ }.execute();
+ }
+
+ private void retrieveMediaAsync(Callback callback) {
+ initializationLock.lock();
+
+ try {
+ if (mCurrentState == State.NON_INITIALIZED) {
+ mCurrentState = State.INITIALIZING;
+
+ int slashPos = CATALOG_URL.lastIndexOf('/');
+ String path = CATALOG_URL.substring(0, slashPos + 1);
+ JSONObject jsonObj = parseUrl(CATALOG_URL);
+
+ JSONArray tracks = jsonObj.getJSONArray(JSON_MUSIC);
+ if (tracks != null) {
+ for (int j = 0; j < tracks.length(); j++) {
+ MediaMetadata item = buildFromJSON(tracks.getJSONObject(j), path);
+ String genre = item.getString(MediaMetadata.METADATA_KEY_GENRE);
+ List<MediaMetadata> list = mMusicListByGenre.get(genre);
+ if (list == null) {
+ list = new ArrayList<>();
+ }
+ list.add(item);
+ mMusicListByGenre.put(genre, list);
+ mMusicListById.put(item.getString(MediaMetadata.METADATA_KEY_MEDIA_ID),
+ item);
+ }
+ }
+ mCurrentState = State.INITIALIZED;
+ }
+ } catch (RuntimeException | JSONException e) {
+ LogHelper.e(TAG, e, "Could not retrieve music list");
+ } finally {
+ if (mCurrentState != State.INITIALIZED) {
+ // Something bad happened, so we reset state to NON_INITIALIZED to allow
+ // retries (eg if the network connection is temporary unavailable)
+ mCurrentState = State.NON_INITIALIZED;
+ }
+ initializationLock.unlock();
+ if (callback != null) {
+ callback.onMusicCatalogReady(mCurrentState == State.INITIALIZED);
+ }
+ }
+ }
+
+ private MediaMetadata buildFromJSON(JSONObject json, String basePath) throws JSONException {
+ String title = json.getString(JSON_TITLE);
+ String album = json.getString(JSON_ALBUM);
+ String artist = json.getString(JSON_ARTIST);
+ String genre = json.getString(JSON_GENRE);
+ String source = json.getString(JSON_SOURCE);
+ String iconUrl = json.getString(JSON_IMAGE);
+ int trackNumber = json.getInt(JSON_TRACK_NUMBER);
+ int totalTrackCount = json.getInt(JSON_TOTAL_TRACK_COUNT);
+ int duration = json.getInt(JSON_DURATION) * 1000; // ms
+
+ LogHelper.d(TAG, "Found music track: ", json);
+
+ // Media is stored relative to JSON file
+ if (!source.startsWith("http")) {
+ source = basePath + source;
+ }
+ if (!iconUrl.startsWith("http")) {
+ iconUrl = basePath + iconUrl;
+ }
+ // Since we don't have a unique ID in the server, we fake one using the hashcode of
+ // the music source. In a real world app, this could come from the server.
+ String id = String.valueOf(source.hashCode());
+
+ // Adding the music source to the MediaMetadata (and consequently using it in the
+ // mediaSession.setMetadata) is not a good idea for a real world music app, because
+ // the session metadata can be accessed by notification listeners. This is done in this
+ // sample for convenience only.
+ return new MediaMetadata.Builder()
+ .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, id)
+ .putString(CUSTOM_METADATA_TRACK_SOURCE, source)
+ .putString(MediaMetadata.METADATA_KEY_ALBUM, album)
+ .putString(MediaMetadata.METADATA_KEY_ARTIST, artist)
+ .putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
+ .putString(MediaMetadata.METADATA_KEY_GENRE, genre)
+ .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, iconUrl)
+ .putString(MediaMetadata.METADATA_KEY_TITLE, title)
+ .putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER, trackNumber)
+ .putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, totalTrackCount)
+ .build();
+ }
+
+ /**
+ * Download a JSON file from a server, parse the content and return the JSON
+ * object.
+ *
+ * @param urlString
+ * @return
+ */
+ private JSONObject parseUrl(String urlString) {
+ InputStream is = null;
+ try {
+ java.net.URL url = new java.net.URL(urlString);
+ URLConnection urlConnection = url.openConnection();
+ is = new BufferedInputStream(urlConnection.getInputStream());
+ BufferedReader reader = new BufferedReader(new InputStreamReader(
+ urlConnection.getInputStream(), "iso-8859-1"), 8);
+ StringBuilder sb = new StringBuilder();
+ String line = null;
+ while ((line = reader.readLine()) != null) {
+ sb.append(line);
+ }
+ return new JSONObject(sb.toString());
+ } catch (Exception e) {
+ LogHelper.e(TAG, "Failed to parse the json for media list", e);
+ return null;
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/MusicDemo/src/main/java/com/example/android/musicservicedemo/utils/BitmapHelper.java b/MusicDemo/src/main/java/com/example/android/musicservicedemo/utils/BitmapHelper.java
new file mode 100644
index 0000000..c743262
--- /dev/null
+++ b/MusicDemo/src/main/java/com/example/android/musicservicedemo/utils/BitmapHelper.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2014 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.example.android.musicservicedemo.utils;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+public class BitmapHelper {
+
+ // Bitmap size for album art in media notifications when there are more than 3 playback actions
+ public static final int MEDIA_ART_SMALL_WIDTH=64;
+ public static final int MEDIA_ART_SMALL_HEIGHT=64;
+
+ // Bitmap size for album art in media notifications when there are no more than 3 playback actions
+ public static final int MEDIA_ART_BIG_WIDTH=128;
+ public static final int MEDIA_ART_BIG_HEIGHT=128;
+
+ public static final Bitmap scaleBitmap(int targetW, int targetH, InputStream is) {
+ // Get the dimensions of the bitmap
+ BitmapFactory.Options bmOptions = new BitmapFactory.Options();
+ bmOptions.inJustDecodeBounds = true;
+ BitmapFactory.decodeStream(is, null, bmOptions);
+ int actualW = bmOptions.outWidth;
+ int actualH = bmOptions.outHeight;
+
+ // Determine how much to scale down the image
+ int scaleFactor = Math.min(actualW/targetW, actualH/targetH);
+
+ // Decode the image file into a Bitmap sized to fill the View
+ bmOptions.inJustDecodeBounds = false;
+ bmOptions.inSampleSize = scaleFactor;
+
+ Bitmap bitmap = BitmapFactory.decodeStream(is, null, bmOptions);
+ return bitmap;
+ }
+
+ public static final Bitmap scaleBitmap(int scaleFactor, InputStream is) {
+ // Get the dimensions of the bitmap
+ BitmapFactory.Options bmOptions = new BitmapFactory.Options();
+
+ // Decode the image file into a Bitmap sized to fill the View
+ bmOptions.inJustDecodeBounds = false;
+ bmOptions.inSampleSize = scaleFactor;
+
+ Bitmap bitmap = BitmapFactory.decodeStream(is, null, bmOptions);
+ return bitmap;
+ }
+
+ public static final int findScaleFactor(int targetW, int targetH, InputStream is) {
+ // Get the dimensions of the bitmap
+ BitmapFactory.Options bmOptions = new BitmapFactory.Options();
+ bmOptions.inJustDecodeBounds = true;
+ BitmapFactory.decodeStream(is, null, bmOptions);
+ int actualW = bmOptions.outWidth;
+ int actualH = bmOptions.outHeight;
+
+ // Determine how much to scale down the image
+ return Math.min(actualW/targetW, actualH/targetH);
+ }
+
+ public static final Bitmap fetchAndRescaleBitmap(String uri, int width, int height)
+ throws IOException {
+ URL url = new URL(uri);
+ HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
+ httpConnection.setDoInput(true);
+ httpConnection.connect();
+ InputStream inputStream = httpConnection.getInputStream();
+ int scaleFactor = findScaleFactor(width, height, inputStream);
+
+ httpConnection = (HttpURLConnection) url.openConnection();
+ httpConnection.setDoInput(true);
+ httpConnection.connect();
+ inputStream = httpConnection.getInputStream();
+ Bitmap bitmap = scaleBitmap(scaleFactor, inputStream);
+ return bitmap;
+ }
+
+}
diff --git a/MusicDemo/src/main/java/com/example/android/musicservicedemo/utils/LogHelper.java b/MusicDemo/src/main/java/com/example/android/musicservicedemo/utils/LogHelper.java
new file mode 100644
index 0000000..4c757f7
--- /dev/null
+++ b/MusicDemo/src/main/java/com/example/android/musicservicedemo/utils/LogHelper.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2014 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.example.android.musicservicedemo.utils;
+
+import android.util.Log;
+
+public class LogHelper {
+ public final static void v(String tag, Object... messages) {
+ log(tag, Log.VERBOSE, null, messages);
+ }
+
+ public final static void d(String tag, Object... messages) {
+ log(tag, Log.DEBUG, null, messages);
+ }
+
+ public final static void i(String tag, Object... messages) {
+ log(tag, Log.INFO, null, messages);
+ }
+
+ public final static void w(String tag, Object... messages) {
+ log(tag, Log.WARN, null, messages);
+ }
+
+ public final static void w(String tag, Throwable t, Object... messages) {
+ log(tag, Log.WARN, t, messages);
+ }
+
+ public final static void e(String tag, Object... messages) {
+ log(tag, Log.ERROR, null, messages);
+ }
+
+ public final static void e(String tag, Throwable t, Object... messages) {
+ log(tag, Log.ERROR, t, messages);
+ }
+
+ public final static void log(String tag, int level, Throwable t, Object... messages) {
+ if (messages != null && Log.isLoggable(tag, level)) {
+ String message = null;
+ if (messages.length == 1) {
+ message = messages[0] == null ? null : messages[0].toString();
+ } else {
+ StringBuilder sb = new StringBuilder();
+ for (Object m: messages) {
+ sb.append(m);
+ }
+ message = sb.toString();
+ }
+ Log.d(tag, message, t);
+ }
+ }
+
+}
diff --git a/MusicDemo/src/main/java/com/example/android/musicservicedemo/utils/MediaIDHelper.java b/MusicDemo/src/main/java/com/example/android/musicservicedemo/utils/MediaIDHelper.java
new file mode 100644
index 0000000..2406886
--- /dev/null
+++ b/MusicDemo/src/main/java/com/example/android/musicservicedemo/utils/MediaIDHelper.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2014 Google Inc. All Rights Reserved.
+ *
+ * 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.example.android.musicservicedemo.utils;
+
+import android.media.MediaMetadata;
+
+/**
+ * Utility class to help on queue related tasks.
+ */
+public class MediaIDHelper {
+
+ private static final String TAG = "MediaIDHelper";
+
+ // Media IDs used on browseable items of MediaBrowser
+ public static final String MEDIA_ID_ROOT = "__ROOT__";
+ public static final String MEDIA_ID_MUSICS_BY_GENRE = "__BY_GENRE__";
+
+ public static final String createTrackMediaID(String categoryType, String categoryValue,
+ MediaMetadata track) {
+ // MediaIDs are of the form <categoryType>/<categoryValue>|<musicUniqueId>, to make it easy to
+ // find the category (like genre) that a music was selected from, so we
+ // can correctly build the playing queue. This is specially useful when
+ // one music can appear in more than one list, like "by genre -> genre_1"
+ // and "by artist -> artist_1".
+ return categoryType + "/" + categoryValue + "|" +
+ track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
+ }
+
+ public static final String createBrowseCategoryMediaID(String categoryType, String categoryValue) {
+ return categoryType + "/" + categoryValue;
+ }
+
+ /**
+ * Extracts unique musicID from the mediaID. mediaID is, by this sample's convention, a
+ * concatenation of category (eg "by_genre"), categoryValue (eg "Classical") and unique
+ * musicID. This is necessary so we know where the user selected the music from, when the music
+ * exists in more than one music list, and thus we are able to correctly build the playing queue.
+ *
+ * @param musicID
+ * @return
+ */
+ public static final String extractMusicIDFromMediaID(String musicID) {
+ String[] segments = musicID.split("\\|", 2);
+ return segments.length == 2 ? segments[1] : null;
+ }
+
+ /**
+ * Extracts category and categoryValue from the mediaID. mediaID is, by this sample's
+ * convention, a concatenation of category (eg "by_genre"), categoryValue (eg "Classical") and
+ * mediaID. This is necessary so we know where the user selected the music from, when the music
+ * exists in more than one music list, and thus we are able to correctly build the playing queue.
+ *
+ * @param mediaID
+ * @return
+ */
+ public static final String[] extractBrowseCategoryFromMediaID(String mediaID) {
+ if (mediaID.indexOf('|') >= 0) {
+ mediaID = mediaID.split("\\|")[0];
+ }
+ if (mediaID.indexOf('/') == 0) {
+ return new String[]{mediaID, null};
+ } else {
+ return mediaID.split("/", 2);
+ }
+ }
+
+ public static final String extractBrowseCategoryValueFromMediaID(String mediaID) {
+ String[] categoryAndValue = extractBrowseCategoryFromMediaID(mediaID);
+ if (categoryAndValue != null && categoryAndValue.length == 2) {
+ return categoryAndValue[1];
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/MusicDemo/src/main/java/com/example/android/musicservicedemo/utils/QueueHelper.java b/MusicDemo/src/main/java/com/example/android/musicservicedemo/utils/QueueHelper.java
new file mode 100644
index 0000000..4dc7a96
--- /dev/null
+++ b/MusicDemo/src/main/java/com/example/android/musicservicedemo/utils/QueueHelper.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2014 Google Inc. All Rights Reserved.
+ *
+ * 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.example.android.musicservicedemo.utils;
+
+import android.media.MediaMetadata;
+import android.media.session.MediaSession;
+
+import com.example.android.musicservicedemo.model.MusicProvider;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import static com.example.android.musicservicedemo.utils.MediaIDHelper.MEDIA_ID_MUSICS_BY_GENRE;
+
+/**
+ * Utility class to help on queue related tasks.
+ */
+public class QueueHelper {
+
+ private static final String TAG = "QueueHelper";
+
+ public static final List<MediaSession.QueueItem> getPlayingQueue(String mediaId,
+ MusicProvider musicProvider) {
+
+ // extract the category and unique music ID from the media ID:
+ String[] category = MediaIDHelper.extractBrowseCategoryFromMediaID(mediaId);
+
+ // This sample only supports genre category.
+ if (!category[0].equals(MEDIA_ID_MUSICS_BY_GENRE) || category.length != 2) {
+ LogHelper.e(TAG, "Could not build a playing queue for this mediaId: ", mediaId);
+ return null;
+ }
+
+ String categoryValue = category[1];
+ LogHelper.e(TAG, "Creating playing queue for musics of genre ", categoryValue);
+
+ List<MediaSession.QueueItem> queue = convertToQueue(
+ musicProvider.getMusicsByGenre(categoryValue));
+
+ return queue;
+ }
+
+ public static final List<MediaSession.QueueItem> getPlayingQueueFromSearch(String query,
+ MusicProvider musicProvider) {
+
+ LogHelper.e(TAG, "Creating playing queue for musics from search ", query);
+
+ return convertToQueue(musicProvider.searchMusics(query));
+ }
+
+
+ public static final int getMusicIndexOnQueue(Iterable<MediaSession.QueueItem> queue,
+ String mediaId) {
+ int index = 0;
+ for (MediaSession.QueueItem item: queue) {
+ if (mediaId.equals(item.getDescription().getMediaId())) {
+ return index;
+ }
+ index++;
+ }
+ return -1;
+ }
+
+ public static final int getMusicIndexOnQueue(Iterable<MediaSession.QueueItem> queue,
+ long queueId) {
+ int index = 0;
+ for (MediaSession.QueueItem item: queue) {
+ if (queueId == item.getQueueId()) {
+ return index;
+ }
+ index++;
+ }
+ return -1;
+ }
+
+ private static final List<MediaSession.QueueItem> convertToQueue(
+ Iterable<MediaMetadata> tracks) {
+ List<MediaSession.QueueItem> queue = new ArrayList<>();
+ int count = 0;
+ for (MediaMetadata track : tracks) {
+ // We don't expect queues to change after created, so we use the item index as the
+ // queueId. Any other number unique in the queue would work.
+ MediaSession.QueueItem item = new MediaSession.QueueItem(
+ track.getDescription(), count++);
+ queue.add(item);
+ }
+ return queue;
+
+ }
+
+ /**
+ * Create a random queue. For simplicity sake, instead of a random queue, we create a
+ * queue using the first genre,
+ *
+ * @param musicProvider
+ * @return
+ */
+ public static final List<MediaSession.QueueItem> getRandomQueue(MusicProvider musicProvider) {
+ Iterator<String> genres = musicProvider.getGenres().iterator();
+ if (!genres.hasNext()) {
+ return new ArrayList<>();
+ }
+ String genre = genres.next();
+ Iterable<MediaMetadata> tracks = musicProvider.getMusicsByGenre(genre);
+
+ return convertToQueue(tracks);
+ }
+
+
+
+ public static final boolean isIndexPlayable(int index, List<MediaSession.QueueItem> queue) {
+ return (queue != null && index >= 0 && index < queue.size());
+ }
+}
\ No newline at end of file
diff --git a/MusicDemo/src/main/res/drawable-hdpi/ic_launcher.png b/MusicDemo/src/main/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..47d6854
--- /dev/null
+++ b/MusicDemo/src/main/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/MusicDemo/src/main/res/drawable-hdpi/ic_notification.png b/MusicDemo/src/main/res/drawable-hdpi/ic_notification.png
new file mode 100644
index 0000000..d8ea5a9
--- /dev/null
+++ b/MusicDemo/src/main/res/drawable-hdpi/ic_notification.png
Binary files differ
diff --git a/MusicDemo/src/main/res/drawable-mdpi/ic_launcher.png b/MusicDemo/src/main/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..01b53fd
--- /dev/null
+++ b/MusicDemo/src/main/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/MusicDemo/src/main/res/drawable-xhdpi/ic_launcher.png b/MusicDemo/src/main/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..af762f2
--- /dev/null
+++ b/MusicDemo/src/main/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/MusicDemo/src/main/res/drawable-xxhdpi/ic_by_genre.png b/MusicDemo/src/main/res/drawable-xxhdpi/ic_by_genre.png
new file mode 100644
index 0000000..da3b4a7
--- /dev/null
+++ b/MusicDemo/src/main/res/drawable-xxhdpi/ic_by_genre.png
Binary files differ
diff --git a/MusicDemo/src/main/res/drawable-xxhdpi/ic_launcher.png b/MusicDemo/src/main/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..eef47aa
--- /dev/null
+++ b/MusicDemo/src/main/res/drawable-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/MusicDemo/src/main/res/values-v21/styles.xml b/MusicDemo/src/main/res/values-v21/styles.xml
new file mode 100644
index 0000000..6169d24
--- /dev/null
+++ b/MusicDemo/src/main/res/values-v21/styles.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 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>
+
+ <style name="AppBaseTheme" parent="Theme.AppCompat.Light">
+ <!-- colorPrimary is used for Notification icon and bottom facet bar icons
+ and overflow actions -->
+ <item name="android:colorPrimary">@color/red</item>
+
+ <!-- colorPrimaryDark is used for background -->
+ <item name="android:colorPrimaryDark">#990000</item>
+
+ <!-- colorAccent is sparingly used for accents, like floating action button highlight,
+ progress on playbar-->
+ <item name="android:colorAccent">#0000FF</item>
+
+ </style>
+
+</resources>
diff --git a/MusicDemo/src/main/res/values/colors.xml b/MusicDemo/src/main/res/values/colors.xml
new file mode 100644
index 0000000..6a5277e
--- /dev/null
+++ b/MusicDemo/src/main/res/values/colors.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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>
+ <color name="red">#ffff0000</color>
+</resources>
diff --git a/MusicDemo/src/main/res/values/strings.xml b/MusicDemo/src/main/res/values/strings.xml
new file mode 100644
index 0000000..82e07b0
--- /dev/null
+++ b/MusicDemo/src/main/res/values/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 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>
+
+ <string name="app_name">Auto Music Demo</string>
+ <string name="favorite">Favorite</string>
+ <string name="error_no_metadata">Unable to retrieve metadata.</string>
+ <string name="browse_genres">Genres</string>
+ <string name="browse_genre_subtitle">Songs by genre</string>
+ <string name="browse_musics_by_genre_subtitle">%1$s songs</string>
+ <string name="random_queue_title">Random music</string>
+ <string name="error_cannot_skip">Cannot skip</string>
+
+</resources>
diff --git a/MusicDemo/src/main/res/values/strings_notifications.xml b/MusicDemo/src/main/res/values/strings_notifications.xml
new file mode 100644
index 0000000..f406ba6
--- /dev/null
+++ b/MusicDemo/src/main/res/values/strings_notifications.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 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>
+
+ <string name="label_pause">Pause</string>
+ <string name="label_play">Play</string>
+ <string name="label_previous">Previous</string>
+ <string name="label_next">Next</string>
+ <string name="error_empty_metadata">Empty metadata!</string>
+</resources>
diff --git a/MusicDemo/src/main/res/values/styles.xml b/MusicDemo/src/main/res/values/styles.xml
new file mode 100644
index 0000000..507dc7b
--- /dev/null
+++ b/MusicDemo/src/main/res/values/styles.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 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>
+
+ <style name="AppBaseTheme" parent="Theme.AppCompat.Light"></style>
+
+ <style name="AppTheme" parent="AppBaseTheme"></style>
+
+</resources>
\ No newline at end of file
diff --git a/MusicDemo/src/main/res/xml/automotive_app_desc.xml b/MusicDemo/src/main/res/xml/automotive_app_desc.xml
new file mode 100644
index 0000000..a84750b
--- /dev/null
+++ b/MusicDemo/src/main/res/xml/automotive_app_desc.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 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.
+ -->
+<automotiveApp>
+ <uses name="media"/>
+</automotiveApp>