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 &lt;automotiveApp&gt; root element. For a media app, this must include
+ *      an &lt;uses name="media"/&gt; element as a child.
+ *      For example, in AndroidManifest.xml:
+ *          &lt;meta-data android:name="com.google.android.gms.car.application"
+ *              android:resource="@xml/automotive_app_desc"/&gt;
+ *      And in res/values/automotive_app_desc.xml:
+ *          &lt;automotiveApp&gt;
+ *              &lt;uses name="media"/&gt;
+ *          &lt;/automotiveApp&gt;
+ *
+ * </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>