Update MediaBrowserService with latest from uAmp

- Refactored the entire sample (except local UI) to reuse code from uAmp;
- Added a custom Android Auto theme;
- Added the new PackageValidator;

Bug: 19870226
Change-Id: Ic402e1aaf303ba1d8fda0dced9696bb9883801b9
diff --git a/media/MediaBrowserService/Application/src/main/AndroidManifest.xml b/media/MediaBrowserService/Application/src/main/AndroidManifest.xml
index 6d05c27..45c8de7 100644
--- a/media/MediaBrowserService/Application/src/main/AndroidManifest.xml
+++ b/media/MediaBrowserService/Application/src/main/AndroidManifest.xml
@@ -36,7 +36,7 @@
             android:resource="@xml/automotive_app_desc"/>
 
 
-        <activity android:name="com.example.android.mediabrowserservice.MusicPlayerActivity"
+        <activity android:name=".MusicPlayerActivity"
                   android:label="@string/app_name">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -51,8 +51,17 @@
             android:name="com.google.android.gms.car.notification.SmallIcon"
             android:resource="@drawable/ic_notification" />
 
+        <!--
+             (OPTIONAL) use this meta data to override the theme from which Android Auto will
+             look for colors. If you don't set this, Android Auto will look
+             for color attributes in your application theme.
+        -->
+        <meta-data
+            android:name="com.google.android.gms.car.application.theme"
+            android:resource="@style/CarTheme" />
+
         <service
-            android:name="com.example.android.mediabrowserservice.MusicService"
+            android:name=".MusicService"
             android:exported="true"
             >
             <intent-filter>
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/AlbumArtCache.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/AlbumArtCache.java
new file mode 100644
index 0000000..4154381
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/AlbumArtCache.java
@@ -0,0 +1,130 @@
+/*
+ * 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.mediabrowserservice;
+
+import android.graphics.Bitmap;
+import android.os.AsyncTask;
+import android.util.LruCache;
+
+import com.example.android.mediabrowserservice.utils.BitmapHelper;
+import com.example.android.mediabrowserservice.utils.LogHelper;
+
+import java.io.IOException;
+
+/**
+ * Implements a basic cache of album arts, with async loading support.
+ */
+public final class AlbumArtCache {
+    private static final String TAG = LogHelper.makeLogTag(AlbumArtCache.class);
+
+    private static final int MAX_ALBUM_ART_CACHE_SIZE = 12*1024*1024;  // 12 MB
+    private static final int MAX_ART_WIDTH = 800;  // pixels
+    private static final int MAX_ART_HEIGHT = 480;  // pixels
+
+    // Resolution reasonable for carrying around as an icon (generally in
+    // MediaDescription.getIconBitmap). This should not be bigger than necessary, because
+    // the MediaDescription object should be lightweight. If you set it too high and try to
+    // serialize the MediaDescription, you may get FAILED BINDER TRANSACTION errors.
+    private static final int MAX_ART_WIDTH_ICON = 128;  // pixels
+    private static final int MAX_ART_HEIGHT_ICON = 128;  // pixels
+
+    private static final int BIG_BITMAP_INDEX = 0;
+    private static final int ICON_BITMAP_INDEX = 1;
+
+    private final LruCache<String, Bitmap[]> mCache;
+
+    private static final AlbumArtCache sInstance = new AlbumArtCache();
+
+    public static AlbumArtCache getInstance() {
+        return sInstance;
+    }
+
+    private AlbumArtCache() {
+        // Holds no more than MAX_ALBUM_ART_CACHE_SIZE bytes, bounded by maxmemory/4 and
+        // Integer.MAX_VALUE:
+        int maxSize = Math.min(MAX_ALBUM_ART_CACHE_SIZE,
+            (int) (Math.min(Integer.MAX_VALUE, Runtime.getRuntime().maxMemory()/4)));
+        mCache = new LruCache<String, Bitmap[]>(maxSize) {
+            @Override
+            protected int sizeOf(String key, Bitmap[] value) {
+                return value[BIG_BITMAP_INDEX].getByteCount()
+                    + value[ICON_BITMAP_INDEX].getByteCount();
+            }
+        };
+    }
+
+    public Bitmap getBigImage(String artUrl) {
+        Bitmap[] result = mCache.get(artUrl);
+        return result == null ? null : result[BIG_BITMAP_INDEX];
+    }
+
+    public Bitmap getIconImage(String artUrl) {
+        Bitmap[] result = mCache.get(artUrl);
+        return result == null ? null : result[ICON_BITMAP_INDEX];
+    }
+
+    public void fetch(final String artUrl, final FetchListener listener) {
+        // WARNING: for the sake of simplicity, simultaneous multi-thread fetch requests
+        // are not handled properly: they may cause redundant costly operations, like HTTP
+        // requests and bitmap rescales. For production-level apps, we recommend you use
+        // a proper image loading library, like Glide.
+        Bitmap[] bitmap = mCache.get(artUrl);
+        if (bitmap != null) {
+            LogHelper.d(TAG, "getOrFetch: album art is in cache, using it", artUrl);
+            listener.onFetched(artUrl, bitmap[BIG_BITMAP_INDEX], bitmap[ICON_BITMAP_INDEX]);
+            return;
+        }
+        LogHelper.d(TAG, "getOrFetch: starting asynctask to fetch ", artUrl);
+
+        new AsyncTask<Void, Void, Bitmap[]>() {
+            @Override
+            protected Bitmap[] doInBackground(Void[] objects) {
+                Bitmap[] bitmaps;
+                try {
+                    Bitmap bitmap = BitmapHelper.fetchAndRescaleBitmap(artUrl,
+                        MAX_ART_WIDTH, MAX_ART_HEIGHT);
+                    Bitmap icon = BitmapHelper.scaleBitmap(bitmap,
+                        MAX_ART_WIDTH_ICON, MAX_ART_HEIGHT_ICON);
+                    bitmaps = new Bitmap[] {bitmap, icon};
+                    mCache.put(artUrl, bitmaps);
+                } catch (IOException e) {
+                    return null;
+                }
+                LogHelper.d(TAG, "doInBackground: putting bitmap in cache. cache size=" +
+                    mCache.size());
+                return bitmaps;
+            }
+
+            @Override
+            protected void onPostExecute(Bitmap[] bitmaps) {
+                if (bitmaps == null) {
+                    listener.onError(artUrl, new IllegalArgumentException("got null bitmaps"));
+                } else {
+                    listener.onFetched(artUrl,
+                        bitmaps[BIG_BITMAP_INDEX], bitmaps[ICON_BITMAP_INDEX]);
+                }
+            }
+        }.execute();
+    }
+
+    public static abstract class FetchListener {
+        public abstract void onFetched(String artUrl, Bitmap bigImage, Bitmap iconImage);
+        public void onError(String artUrl, Exception e) {
+            LogHelper.e(TAG, e, "AlbumArtFetchListener: error while downloading " + artUrl);
+        }
+    }
+}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MediaNotificationManager.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MediaNotificationManager.java
index aec4691..59da728 100644
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MediaNotificationManager.java
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MediaNotificationManager.java
@@ -23,10 +23,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Color;
@@ -35,13 +31,9 @@
 import android.media.session.MediaController;
 import android.media.session.MediaSession;
 import android.media.session.PlaybackState;
-import android.os.AsyncTask;
-import android.util.LruCache;
 
-import com.example.android.mediabrowserservice.utils.BitmapHelper;
 import com.example.android.mediabrowserservice.utils.LogHelper;
-
-import java.io.IOException;
+import com.example.android.mediabrowserservice.utils.ResourceHelper;
 
 /**
  * Keeps track of a notification and updates it automatically for a given
@@ -49,31 +41,30 @@
  * won't be killed during playback.
  */
 public class MediaNotificationManager extends BroadcastReceiver {
-    private static final String TAG = LogHelper.makeLogTag(MediaNotificationManager.class.getSimpleName());
+    private static final String TAG = LogHelper.makeLogTag(MediaNotificationManager.class);
 
     private static final int NOTIFICATION_ID = 412;
+    private static final int REQUEST_CODE = 100;
 
     public static final String ACTION_PAUSE = "com.example.android.mediabrowserservice.pause";
     public static final String ACTION_PLAY = "com.example.android.mediabrowserservice.play";
     public static final String ACTION_PREV = "com.example.android.mediabrowserservice.prev";
     public static final String ACTION_NEXT = "com.example.android.mediabrowserservice.next";
 
-    private static final int MAX_ALBUM_ART_CACHE_SIZE = 1024*1024;
-
     private final MusicService mService;
     private MediaSession.Token mSessionToken;
     private MediaController mController;
     private MediaController.TransportControls mTransportControls;
-    private final LruCache<String, Bitmap> mAlbumArtCache;
 
     private PlaybackState mPlaybackState;
     private MediaMetadata mMetadata;
 
-    private Notification.Builder mNotificationBuilder;
     private NotificationManager mNotificationManager;
-    private Notification.Action mPlayPauseAction;
 
-    private PendingIntent mPauseIntent, mPlayIntent, mPreviousIntent, mNextIntent;
+    private PendingIntent mPauseIntent;
+    private PendingIntent mPlayIntent;
+    private PendingIntent mPreviousIntent;
+    private PendingIntent mNextIntent;
 
     private int mNotificationColor;
 
@@ -83,48 +74,25 @@
         mService = service;
         updateSessionToken();
 
-        // simple album art cache that holds no more than
-        // MAX_ALBUM_ART_CACHE_SIZE bytes:
-        mAlbumArtCache = new LruCache<String, Bitmap>(MAX_ALBUM_ART_CACHE_SIZE) {
-            @Override
-            protected int sizeOf(String key, Bitmap value) {
-                return value.getByteCount();
-            }
-        };
-
-        mNotificationColor = getNotificationColor();
+        mNotificationColor = ResourceHelper.getThemeColor(mService,
+            android.R.attr.colorPrimary, Color.DKGRAY);
 
         mNotificationManager = (NotificationManager) mService
                 .getSystemService(Context.NOTIFICATION_SERVICE);
 
         String pkg = mService.getPackageName();
-        mPauseIntent = PendingIntent.getBroadcast(mService, 100,
+        mPauseIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE,
                 new Intent(ACTION_PAUSE).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT);
-        mPlayIntent = PendingIntent.getBroadcast(mService, 100,
+        mPlayIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE,
                 new Intent(ACTION_PLAY).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT);
-        mPreviousIntent = PendingIntent.getBroadcast(mService, 100,
+        mPreviousIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE,
                 new Intent(ACTION_PREV).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT);
-        mNextIntent = PendingIntent.getBroadcast(mService, 100,
+        mNextIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE,
                 new Intent(ACTION_NEXT).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT);
-    }
 
-    protected int getNotificationColor() {
-        int notificationColor = 0;
-        String packageName = mService.getPackageName();
-        try {
-            Context packageContext = mService.createPackageContext(packageName, 0);
-            ApplicationInfo applicationInfo =
-                    mService.getPackageManager().getApplicationInfo(packageName, 0);
-            packageContext.setTheme(applicationInfo.theme);
-            Resources.Theme theme = packageContext.getTheme();
-            TypedArray ta = theme.obtainStyledAttributes(
-                    new int[] {android.R.attr.colorPrimary});
-            notificationColor = ta.getColor(0, Color.DKGRAY);
-            ta.recycle();
-        } catch (PackageManager.NameNotFoundException e) {
-            e.printStackTrace();
-        }
-        return notificationColor;
+        // Cancel all notifications to handle the case where the Service was killed and
+        // restarted by the system.
+        mNotificationManager.cancelAll();
     }
 
     /**
@@ -134,20 +102,23 @@
      */
     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();
+            Notification notification = createNotification();
+            if (notification != null) {
+                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);
+
+                mService.startForeground(NOTIFICATION_ID, notification);
+                mStarted = true;
+            }
         }
     }
 
@@ -156,29 +127,38 @@
      * was destroyed this has no effect.
      */
     public void stopNotification() {
-        mStarted = false;
-        mController.unregisterCallback(mCb);
-        try {
-            mNotificationManager.cancel(NOTIFICATION_ID);
-            mService.unregisterReceiver(this);
-        } catch (IllegalArgumentException ex) {
-            // ignore if the receiver is not registered.
+        if (mStarted) {
+            mStarted = false;
+            mController.unregisterCallback(mCb);
+            try {
+                mNotificationManager.cancel(NOTIFICATION_ID);
+                mService.unregisterReceiver(this);
+            } catch (IllegalArgumentException ex) {
+                // ignore if the receiver is not registered.
+            }
+            mService.stopForeground(true);
         }
-        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();
+        switch (action) {
+            case ACTION_PAUSE:
+                mTransportControls.pause();
+                break;
+            case ACTION_PLAY:
+                mTransportControls.play();
+                break;
+            case ACTION_NEXT:
+                mTransportControls.skipToNext();
+                break;
+            case ACTION_PREV:
+                mTransportControls.skipToPrevious();
+                break;
+            default:
+                LogHelper.w(TAG, "Unknown intent ignored. Action=", action);
         }
     }
 
@@ -202,19 +182,37 @@
         }
     }
 
+    private PendingIntent createContentIntent() {
+        Intent openUI = new Intent(mService, MusicPlayerActivity.class);
+        openUI.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+        return PendingIntent.getActivity(mService, REQUEST_CODE, openUI,
+                PendingIntent.FLAG_CANCEL_CURRENT);
+    }
+
     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();
+            if (state != null && (state.getState() == PlaybackState.STATE_STOPPED ||
+                    state.getState() == PlaybackState.STATE_NONE)) {
+                stopNotification();
+            } else {
+                Notification notification = createNotification();
+                if (notification != null) {
+                    mNotificationManager.notify(NOTIFICATION_ID, notification);
+                }
+            }
         }
 
         @Override
         public void onMetadataChanged(MediaMetadata metadata) {
             mMetadata = metadata;
             LogHelper.d(TAG, "Received new metadata ", metadata);
-            updateNotificationMetadata();
+            Notification notification = createNotification();
+            if (notification != null) {
+                mNotificationManager.notify(NOTIFICATION_ID, notification);
+            }
         }
 
         @Override
@@ -225,71 +223,76 @@
         }
     };
 
-    private void updateNotificationMetadata() {
+    private Notification createNotification() {
         LogHelper.d(TAG, "updateNotificationMetadata. mMetadata=" + mMetadata);
         if (mMetadata == null || mPlaybackState == null) {
-            return;
+            return null;
         }
 
-        updatePlayPauseAction();
-
-        mNotificationBuilder = new Notification.Builder(mService);
-        int playPauseActionIndex = 0;
+        Notification.Builder notificationBuilder = new Notification.Builder(mService);
+        int playPauseButtonPosition = 0;
 
         // If skip to previous action is enabled
         if ((mPlaybackState.getActions() & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0) {
-            mNotificationBuilder
-                    .addAction(R.drawable.ic_skip_previous_white_24dp,
-                            mService.getString(R.string.label_previous), mPreviousIntent);
-            playPauseActionIndex = 1;
+            notificationBuilder.addAction(R.drawable.ic_skip_previous_white_24dp,
+                        mService.getString(R.string.label_previous), mPreviousIntent);
+
+            // If there is a "skip to previous" button, the play/pause button will
+            // be the second one. We need to keep track of it, because the MediaStyle notification
+            // requires to specify the index of the buttons (actions) that should be visible
+            // when in compact view.
+            playPauseButtonPosition = 1;
         }
 
-        mNotificationBuilder.addAction(mPlayPauseAction);
+        addPlayPauseAction(notificationBuilder);
 
         // If skip to next action is enabled
         if ((mPlaybackState.getActions() & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) {
-            mNotificationBuilder.addAction(R.drawable.ic_skip_next_white_24dp,
-                    mService.getString(R.string.label_next), mNextIntent);
+            notificationBuilder.addAction(R.drawable.ic_skip_next_white_24dp,
+                mService.getString(R.string.label_next), mNextIntent);
         }
 
         MediaDescription description = mMetadata.getDescription();
 
         String fetchArtUrl = null;
-        Bitmap art = description.getIconBitmap();
-        if (art == null && description.getIconUri() != null) {
+        Bitmap art = null;
+        if (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.
             // async fetch the album art icon
             String artUrl = description.getIconUri().toString();
-            art = mAlbumArtCache.get(artUrl);
+            art = AlbumArtCache.getInstance().getBigImage(artUrl);
             if (art == null) {
                 fetchArtUrl = artUrl;
                 // use a placeholder art while the remote art is being downloaded
-                art = BitmapFactory.decodeResource(mService.getResources(), R.drawable.ic_default_art);
+                art = BitmapFactory.decodeResource(mService.getResources(),
+                    R.drawable.ic_default_art);
             }
         }
 
-        mNotificationBuilder
+        notificationBuilder
                 .setStyle(new Notification.MediaStyle()
-                        .setShowActionsInCompactView(playPauseActionIndex)  // only show play/pause in compact view
-                        .setMediaSession(mSessionToken))
+                    .setShowActionsInCompactView(
+                        new int[]{playPauseButtonPosition})  // show only play/pause in compact view
+                    .setMediaSession(mSessionToken))
                 .setColor(mNotificationColor)
                 .setSmallIcon(R.drawable.ic_notification)
                 .setVisibility(Notification.VISIBILITY_PUBLIC)
                 .setUsesChronometer(true)
+                .setContentIntent(createContentIntent())
                 .setContentTitle(description.getTitle())
                 .setContentText(description.getSubtitle())
                 .setLargeIcon(art);
 
-        updateNotificationPlaybackState();
-
-        mService.startForeground(NOTIFICATION_ID, mNotificationBuilder.build());
+        setNotificationPlaybackState(notificationBuilder);
         if (fetchArtUrl != null) {
-            fetchBitmapFromURLAsync(fetchArtUrl);
+            fetchBitmapFromURLAsync(fetchArtUrl, notificationBuilder);
         }
+
+        return notificationBuilder.build();
     }
 
-    private void updatePlayPauseAction() {
+    private void addPlayPauseAction(Notification.Builder builder) {
         LogHelper.d(TAG, "updatePlayPauseAction");
         String label;
         int icon;
@@ -303,78 +306,49 @@
             icon = R.drawable.ic_play_arrow_white_24dp;
             intent = mPlayIntent;
         }
-        if (mPlayPauseAction == null) {
-            mPlayPauseAction = new Notification.Action(icon, label, intent);
-        } else {
-            mPlayPauseAction.icon = icon;
-            mPlayPauseAction.title = label;
-            mPlayPauseAction.actionIntent = intent;
-        }
+        builder.addAction(new Notification.Action(icon, label, intent));
     }
 
-    private void updateNotificationPlaybackState() {
+    private void setNotificationPlaybackState(Notification.Builder builder) {
         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) {
+        if (mPlaybackState.getState() == PlaybackState.STATE_PLAYING
+                && 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);
+            builder
+                .setWhen(System.currentTimeMillis() - mPlaybackState.getPosition())
+                .setShowWhen(true)
+                .setUsesChronometer(true);
         } else {
             LogHelper.d(TAG, "updateNotificationPlaybackState. hiding playback position");
-            mNotificationBuilder
-                    .setWhen(0)
-                    .setShowWhen(false)
-                    .setUsesChronometer(false);
+            builder
+                .setWhen(0)
+                .setShowWhen(false)
+                .setUsesChronometer(false);
         }
 
-        updatePlayPauseAction();
-
         // Make sure that the notification can be dismissed by the user when we are not playing:
-        mNotificationBuilder.setOngoing(mPlaybackState.getState() == PlaybackState.STATE_PLAYING);
-
-        mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
+        builder.setOngoing(mPlaybackState.getState() == PlaybackState.STATE_PLAYING);
     }
 
-    public void fetchBitmapFromURLAsync(final String source) {
-        LogHelper.d(TAG, "getBitmapFromURLAsync: starting asynctask to fetch ", source);
-        new AsyncTask<Void, Void, Bitmap>() {
+    private void fetchBitmapFromURLAsync(final String bitmapUrl,
+                                         final Notification.Builder builder) {
+        AlbumArtCache.getInstance().fetch(bitmapUrl, new AlbumArtCache.FetchListener() {
             @Override
-            protected Bitmap doInBackground(Void[] objects) {
-                Bitmap bitmap = null;
-                try {
-                    bitmap = BitmapHelper.fetchAndRescaleBitmap(source,
-                            BitmapHelper.MEDIA_ART_BIG_WIDTH, BitmapHelper.MEDIA_ART_BIG_HEIGHT);
-                    mAlbumArtCache.put(source, bitmap);
-                } catch (IOException e) {
-                    LogHelper.e(TAG, e, "getBitmapFromURLAsync: " + source);
-                }
-                return bitmap;
-            }
-
-            @Override
-            protected void onPostExecute(Bitmap bitmap) {
-                if (bitmap != null && mMetadata != null &&
-                        mNotificationBuilder != null && mMetadata.getDescription() != null &&
-                        !source.equals(mMetadata.getDescription().getIconUri())) {
+            public void onFetched(String artUrl, Bitmap bitmap, Bitmap icon) {
+                if (mMetadata != null && mMetadata.getDescription() != null &&
+                    artUrl.equals(mMetadata.getDescription().getIconUri().toString())) {
                     // If the media is still the same, update the notification:
-                    LogHelper.d(TAG, "getBitmapFromURLAsync: set bitmap to ", source);
-                    mNotificationBuilder.setLargeIcon(bitmap);
-                    mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build());
+                    LogHelper.d(TAG, "fetchBitmapFromURLAsync: set bitmap to ", artUrl);
+                    builder.setLargeIcon(bitmap);
+                    mNotificationManager.notify(NOTIFICATION_ID, builder.build());
                 }
             }
-        }.execute();
+        });
     }
-
 }
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MusicService.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MusicService.java
index b055d69..83606bd 100644
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MusicService.java
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/MusicService.java
@@ -1,4 +1,4 @@
-/*
+ /*
  * Copyright (C) 2014 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,932 +16,713 @@
 
 package com.example.android.mediabrowserservice;
 
-import android.content.Context;
-import android.content.Intent;
-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.Handler;
-import android.os.Message;
-import android.os.PowerManager;
-import android.os.SystemClock;
-import android.service.media.MediaBrowserService;
+ import android.app.PendingIntent;
+ import android.content.Context;
+ import android.content.Intent;
+ import android.graphics.Bitmap;
+ import android.media.MediaDescription;
+ import android.media.MediaMetadata;
+ import android.media.browse.MediaBrowser.MediaItem;
+ import android.media.session.MediaSession;
+ import android.media.session.PlaybackState;
+ import android.net.Uri;
+ import android.os.Bundle;
+ import android.os.Handler;
+ import android.os.Message;
+ import android.os.SystemClock;
+ import android.service.media.MediaBrowserService;
 
-import com.example.android.mediabrowserservice.model.MusicProvider;
-import com.example.android.mediabrowserservice.utils.LogHelper;
-import com.example.android.mediabrowserservice.utils.MediaIDHelper;
-import com.example.android.mediabrowserservice.utils.QueueHelper;
+ import com.example.android.mediabrowserservice.model.MusicProvider;
+ import com.example.android.mediabrowserservice.utils.CarHelper;
+ import com.example.android.mediabrowserservice.utils.LogHelper;
+ import com.example.android.mediabrowserservice.utils.MediaIDHelper;
+ import com.example.android.mediabrowserservice.utils.QueueHelper;
 
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
+ import java.lang.ref.WeakReference;
+ import java.util.ArrayList;
+ import java.util.Collections;
+ import java.util.List;
 
-import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_MUSICS_BY_GENRE;
-import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_ROOT;
-import static com.example.android.mediabrowserservice.utils.MediaIDHelper.createBrowseCategoryMediaID;
-import static com.example.android.mediabrowserservice.utils.MediaIDHelper.extractBrowseCategoryFromMediaID;
+ import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_MUSICS_BY_GENRE;
+ import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_ROOT;
+ import static com.example.android.mediabrowserservice.utils.MediaIDHelper.createBrowseCategoryMediaID;
 
-/**
- * This class provides a MediaBrowser through a service. It exposes the media library to a browsing
- * client, through the onGetRoot and onLoadChildren methods. It also creates a MediaSession and
- * exposes it through its MediaSession.Token, which allows the client to create a MediaController
- * that connects to and send control commands to the MediaSession remotely. This is useful for
- * user interfaces that need to interact with your media session, like Android Auto. You can
- * (should) also use the same service from your app's UI, which gives a seamless playback
- * experience to the user.
- *
- * To implement a MediaBrowserService, you need 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> In onCreate, 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> Declare and export the service in AndroidManifest with an intent receiver for the action
- *      android.media.browse.MediaBrowserService
- *
- * </ul>
- *
- * To make your app compatible with Android Auto, you also need to:
- *
- * <ul>
- *
- * <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>
+ /**
+  * This class provides a MediaBrowser through a service. It exposes the media library to a browsing
+  * client, through the onGetRoot and onLoadChildren methods. It also creates a MediaSession and
+  * exposes it through its MediaSession.Token, which allows the client to create a MediaController
+  * that connects to and send control commands to the MediaSession remotely. This is useful for
+  * user interfaces that need to interact with your media session, like Android Auto. You can
+  * (should) also use the same service from your app's UI, which gives a seamless playback
+  * experience to the user.
+  *
+  * To implement a MediaBrowserService, you need 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> In onCreate, 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> Declare and export the service in AndroidManifest with an intent receiver for the action
+  *      android.media.browse.MediaBrowserService
+  *
+  * </ul>
+  *
+  * To make your app compatible with Android Auto, you also need to:
+  *
+  * <ul>
+  *
+  * <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>
 
- * @see <a href="README.md">README.md</a> for more details.
- *
- */
+  * @see <a href="README.md">README.md</a> for more details.
+  *
+  */
 
-public class MusicService extends MediaBrowserService implements OnPreparedListener,
-        OnCompletionListener, OnErrorListener, AudioManager.OnAudioFocusChangeListener {
+ public class MusicService extends MediaBrowserService implements Playback.Callback {
 
-    private static final String TAG = LogHelper.makeLogTag(MusicService.class.getSimpleName());
+     // The action of the incoming Intent indicating that it contains a command
+     // to be executed (see {@link #onStartCommand})
+     public static final String ACTION_CMD = "com.example.android.mediabrowserservice.ACTION_CMD";
+     // The key in the extras of the incoming Intent indicating the command that
+     // should be executed (see {@link #onStartCommand})
+     public static final String CMD_NAME = "CMD_NAME";
+     // A value of a CMD_NAME key in the extras of the incoming Intent that
+     // indicates that the music playback should be paused (see {@link #onStartCommand})
+     public static final String CMD_PAUSE = "CMD_PAUSE";
 
-    // Action to thumbs up a media item
-    private static final String CUSTOM_ACTION_THUMBS_UP = "thumbs_up";
-    // Delay stopSelf by using a handler.
-    private static final int STOP_DELAY = 30000;
+     private static final String TAG = LogHelper.makeLogTag(MusicService.class);
+     // Action to thumbs up a media item
+     private static final String CUSTOM_ACTION_THUMBS_UP =
+         "com.example.android.mediabrowserservice.THUMBS_UP";
+     // Delay stopSelf by using a handler.
+     private static final int STOP_DELAY = 30000;
 
-    // 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;
+     // Music catalog manager
+     private MusicProvider mMusicProvider;
+     private MediaSession mSession;
+     // "Now playing" queue:
+     private List<MediaSession.QueueItem> mPlayingQueue;
+     private int mCurrentIndexOnQueue;
+     private MediaNotificationManager mMediaNotificationManager;
+     // Indicates whether the service was started.
+     private boolean mServiceStarted;
+     private DelayedStopHandler mDelayedStopHandler = new DelayedStopHandler(this);
+     private Playback mPlayback;
+     private PackageValidator mPackageValidator;
 
-    // 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_SIMULATOR_PACKAGE_NAME = "com.google.android.mediasimulator";
+     /*
+      * (non-Javadoc)
+      * @see android.app.Service#onCreate()
+      */
+     @Override
+     public void onCreate() {
+         super.onCreate();
+         LogHelper.d(TAG, "onCreate");
 
-    // Music catalog manager
-    private MusicProvider mMusicProvider;
+         mPlayingQueue = new ArrayList<>();
+         mMusicProvider = new MusicProvider();
+         mPackageValidator = new PackageValidator(this);
 
-    private MediaSession mSession;
-    private MediaPlayer mMediaPlayer;
+         // Start a new MediaSession
+         mSession = new MediaSession(this, "MusicService");
+         setSessionToken(mSession.getSessionToken());
+         mSession.setCallback(new MediaSessionCallback());
+         mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS |
+             MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
 
-    // "Now playing" queue:
-    private List<MediaSession.QueueItem> mPlayingQueue;
-    private int mCurrentIndexOnQueue;
+         mPlayback = new Playback(this, mMusicProvider);
+         mPlayback.setState(PlaybackState.STATE_NONE);
+         mPlayback.setCallback(this);
+         mPlayback.start();
 
-    // Current local media player state
-    private int mState = PlaybackState.STATE_NONE;
+         Context context = getApplicationContext();
+         Intent intent = new Intent(context, MusicPlayerActivity.class);
+         PendingIntent pi = PendingIntent.getActivity(context, 99 /*request code*/,
+                 intent, PendingIntent.FLAG_UPDATE_CURRENT);
+         mSession.setSessionActivity(pi);
 
-    // 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;
+         Bundle extras = new Bundle();
+         CarHelper.setSlotReservationFlags(extras, true, true, true);
+         mSession.setExtras(extras);
 
-    private MediaNotificationManager mMediaNotificationManager;
+         updatePlaybackState(null);
 
-    // Indicates whether the service was started.
-    private boolean mServiceStarted;
+         mMediaNotificationManager = new MediaNotificationManager(this);
+     }
 
-    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
-    }
+     /**
+      * (non-Javadoc)
+      * @see android.app.Service#onStartCommand(android.content.Intent, int, int)
+      */
+     @Override
+     public int onStartCommand(Intent startIntent, int flags, int startId) {
+         if (startIntent != null) {
+             String action = startIntent.getAction();
+             String command = startIntent.getStringExtra(CMD_NAME);
+             if (ACTION_CMD.equals(action)) {
+                 if (CMD_PAUSE.equals(command)) {
+                     if (mPlayback != null && mPlayback.isPlaying()) {
+                         handlePauseRequest();
+                     }
+                 }
+             }
+         }
+         return START_STICKY;
+     }
 
-    // Type of audio focus we have:
-    private AudioFocus mAudioFocus = AudioFocus.NoFocusNoDuck;
-    private AudioManager mAudioManager;
+     /**
+      * (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);
 
-    // Indicates if we should start playing immediately after we gain focus.
-    private boolean mPlayOnFocusGain;
+         mDelayedStopHandler.removeCallbacksAndMessages(null);
+         // Always release the MediaSession to clean up resources
+         // and notify associated MediaController(s).
+         mSession.release();
+     }
 
-    private Handler mDelayedStopHandler = new Handler() {
-        @Override
-        public void handleMessage(Message msg) {
-            if ((mMediaPlayer != null && mMediaPlayer.isPlaying()) ||
-                    mPlayOnFocusGain) {
-                LogHelper.d(TAG, "Ignoring delayed stop since the media player is in use.");
-                return;
-            }
-            LogHelper.d(TAG, "Stopping service with delay handler.");
-            stopSelf();
-            mServiceStarted = false;
-        }
-    };
+     @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 (!mPackageValidator.isCallerAllowed(this, clientPackageName, clientUid)) {
+             // 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;
+         }
+         //noinspection StatementWithEmptyBody
+         if (CarHelper.isValidCarPackage(clientPackageName)) {
+             // Optional: if your app needs to adapt ads, music library or anything else that
+             // needs to run differently when connected to the car, this is where you should handle
+             // it.
+         }
+         return new BrowserRoot(MEDIA_ID_ROOT, null);
+     }
 
-    /*
-     * (non-Javadoc)
-     * @see android.app.Service#onCreate()
-     */
-    @Override
-    public void onCreate() {
-        super.onCreate();
-        LogHelper.d(TAG, "onCreate");
+     @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();
 
-        mPlayingQueue = new ArrayList<>();
+             mMusicProvider.retrieveMediaAsync(new MusicProvider.Callback() {
+                 @Override
+                 public void onMusicCatalogReady(boolean success) {
+                     if (success) {
+                         loadChildrenImpl(parentMediaId, result);
+                     } else {
+                         updatePlaybackState(getString(R.string.error_no_metadata));
+                         result.sendResult(Collections.<MediaItem>emptyList());
+                     }
+                 }
+             });
 
-        // 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");
+         } 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<MediaItem>> result) {
+         LogHelper.d(TAG, "OnLoadChildren: parentMediaId=", parentMediaId);
 
-        // Create the music catalog metadata provider
-        mMusicProvider = new MusicProvider();
-        mMusicProvider.retrieveMedia(new MusicProvider.Callback() {
-            @Override
-            public void onMusicCatalogReady(boolean success) {
-                mState = success ? PlaybackState.STATE_NONE : PlaybackState.STATE_ERROR;
-            }
-        });
+         List<MediaItem> mediaItems = new ArrayList<>();
 
-        mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+         if (MEDIA_ID_ROOT.equals(parentMediaId)) {
+             LogHelper.d(TAG, "OnLoadChildren.ROOT");
+             mediaItems.add(new MediaItem(
+                     new MediaDescription.Builder()
+                         .setMediaId(MEDIA_ID_MUSICS_BY_GENRE)
+                         .setTitle(getString(R.string.browse_genres))
+                         .setIconUri(Uri.parse("android.resource://" +
+                             "com.example.android.mediabrowserservice/drawable/ic_by_genre"))
+                         .setSubtitle(getString(R.string.browse_genre_subtitle))
+                         .build(), MediaItem.FLAG_BROWSABLE
+             ));
 
-        // Start a new MediaSession
-        mSession = new MediaSession(this, "MusicService");
-        setSessionToken(mSession.getSessionToken());
-        mSession.setCallback(new MediaSessionCallback());
-        mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS |
-                MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
+         } else if (MEDIA_ID_MUSICS_BY_GENRE.equals(parentMediaId)) {
+             LogHelper.d(TAG, "OnLoadChildren.GENRES");
+             for (String genre : mMusicProvider.getGenres()) {
+                 MediaItem item = new 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(), MediaItem.FLAG_BROWSABLE
+                 );
+                 mediaItems.add(item);
+             }
 
-        // Use these extras to reserve space for the corresponding actions, even when they are disabled
-        // in the playbackstate, so the custom actions don't reflow.
-        Bundle extras = new Bundle();
-        extras.putBoolean(
-            "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT",
-            true);
-        extras.putBoolean(
-            "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS",
-            true);
-        // If you want to reserve the Queue slot when there is no queue
-        // (mSession.setQueue(emptylist)), uncomment the lines below:
-        // extras.putBoolean(
-        //   "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE",
-        //   true);
-        mSession.setExtras(extras);
+         } else if (parentMediaId.startsWith(MEDIA_ID_MUSICS_BY_GENRE)) {
+             String genre = MediaIDHelper.getHierarchy(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.createMediaID(
+                         track.getDescription().getMediaId(), MEDIA_ID_MUSICS_BY_GENRE, genre);
+                 MediaMetadata trackCopy = new MediaMetadata.Builder(track)
+                         .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, hierarchyAwareMediaID)
+                         .build();
+                 MediaItem bItem = new MediaItem(
+                         trackCopy.getDescription(), MediaItem.FLAG_PLAYABLE);
+                 mediaItems.add(bItem);
+             }
+         } else {
+             LogHelper.w(TAG, "Skipping unmatched parentMediaId: ", parentMediaId);
+         }
+         LogHelper.d(TAG, "OnLoadChildren sending ", mediaItems.size(),
+                 " results for ", parentMediaId);
+         result.sendResult(mediaItems);
+     }
 
-        updatePlaybackState(null);
+     private final class MediaSessionCallback extends MediaSession.Callback {
+         @Override
+         public void onPlay() {
+             LogHelper.d(TAG, "play");
 
-        mMediaNotificationManager = new MediaNotificationManager(this);
-    }
+             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;
+             }
 
-    /*
-     * (non-Javadoc)
-     * @see android.app.Service#onDestroy()
-     */
-    @Override
-    public void onDestroy() {
-        LogHelper.d(TAG, "onDestroy");
+             if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
+                 handlePlayRequest();
+             }
+         }
 
-        // Service is being killed, so make sure we release our resources
-        handleStopRequest(null);
+         @Override
+         public void onSkipToQueueItem(long queueId) {
+             LogHelper.d(TAG, "OnSkipToQueueItem:" + queueId);
 
-        mDelayedStopHandler.removeCallbacksAndMessages(null);
-        // In particular, always release the MediaSession to clean up resources
-        // and notify associated MediaController(s).
-        mSession.release();
-    }
+             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 onSeekTo(long position) {
+             LogHelper.d(TAG, "onSeekTo:", position);
+             mPlayback.seekTo((int) position);
+         }
 
-    @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 (!PackageValidator.isCallerAllowed(this, clientPackageName, clientUid)) {
-            // 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;
-        }
-        if (ANDROID_AUTO_PACKAGE_NAME.equals(clientPackageName)) {
-            // Optional: if your app needs to adapt ads, music library or anything else that
-            // needs to run differently when connected to the car, this is where you should handle
-            // it.
-        }
-        return new BrowserRoot(MEDIA_ID_ROOT, null);
-    }
+         @Override
+         public void onPlayFromMediaId(String mediaId, Bundle extras) {
+             LogHelper.d(TAG, "playFromMediaId mediaId:", mediaId, "  extras=", extras);
 
-    @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();
+             // 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);
 
-            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>());
-                    }
-                }
-            });
+             if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
+                 // set the current index on queue from the media Id:
+                 mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue(mPlayingQueue, mediaId);
 
-        } else {
-            // If our music catalog is already loaded/cached, load them into result immediately
-            loadChildrenImpl(parentMediaId, result);
-        }
-    }
+                 if (mCurrentIndexOnQueue < 0) {
+                     LogHelper.e(TAG, "playFromMediaId: media ID ", mediaId,
+                             " could not be found on queue. Ignoring.");
+                 } else {
+                     // play the music
+                     handlePlayRequest();
+                 }
+             }
+         }
 
-    /**
-     * 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);
+         @Override
+         public void onPause() {
+             LogHelper.d(TAG, "pause. current state=" + mPlayback.getState());
+             handlePauseRequest();
+         }
 
-        List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>();
+         @Override
+         public void onStop() {
+             LogHelper.d(TAG, "stop. current state=" + mPlayback.getState());
+             handleStopRequest(null);
+         }
 
-        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.mediabrowserservice/drawable/ic_by_genre"))
-                        .setSubtitle(getString(R.string.browse_genre_subtitle))
-                        .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE
-            ));
+         @Override
+         public void onSkipToNext() {
+             LogHelper.d(TAG, "skipToNext");
+             mCurrentIndexOnQueue++;
+             if (mPlayingQueue != null && mCurrentIndexOnQueue >= mPlayingQueue.size()) {
+                 // This sample's behavior: skipping to next when in last song returns to the
+                 // first song.
+                 mCurrentIndexOnQueue = 0;
+             }
+             if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
+                 handlePlayRequest();
+             } else {
+                 LogHelper.e(TAG, "skipToNext: cannot skip to next. next Index=" +
+                         mCurrentIndexOnQueue + " queue length=" +
+                         (mPlayingQueue == null ? "null" : mPlayingQueue.size()));
+                 handleStopRequest("Cannot skip");
+             }
+         }
 
-        } 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);
-            }
+         @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)) {
+                 handlePlayRequest();
+             } else {
+                 LogHelper.e(TAG, "skipToPrevious: cannot skip to previous. previous Index=" +
+                         mCurrentIndexOnQueue + " queue length=" +
+                         (mPlayingQueue == null ? "null" : mPlayingQueue.size()));
+                 handleStopRequest("Cannot skip");
+             }
+         }
 
-        } 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);
-    }
+         @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 musicId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
+                     mMusicProvider.setFavorite(musicId, !mMusicProvider.isFavorite(musicId));
+                 }
+                 // playback state needs to be updated because the "Favorite" icon on the
+                 // custom action will change to reflect the new favorite state.
+                 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);
 
-    private final class MediaSessionCallback extends MediaSession.Callback {
-        @Override
-        public void onPlay() {
-            LogHelper.d(TAG, "play");
+             if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
+                 // start playing from the beginning of the queue
+                 mCurrentIndexOnQueue = 0;
 
-            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;
-            }
+                 handlePlayRequest();
+             }
+         }
+     }
 
-            if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
-                handlePlayRequest();
-            }
-        }
+     /**
+      * Handle a request to play music
+      */
+     private void handlePlayRequest() {
+         LogHelper.d(TAG, "handlePlayRequest: mState=" + mPlayback.getState());
 
-        @Override
-        public void onSkipToQueueItem(long queueId) {
-            LogHelper.d(TAG, "OnSkipToQueueItem:" + queueId);
+         mDelayedStopHandler.removeCallbacksAndMessages(null);
+         if (!mServiceStarted) {
+             LogHelper.v(TAG, "Starting service");
+             // The MusicService needs to keep running even after the calling MediaBrowser
+             // is disconnected. Call startService(Intent) and then stopSelf(..) when we no longer
+             // need to play media.
+             startService(new Intent(getApplicationContext(), MusicService.class));
+             mServiceStarted = true;
+         }
 
-            if (mState == PlaybackState.STATE_PAUSED) {
-                mState = PlaybackState.STATE_STOPPED;
-            }
+         if (!mSession.isActive()) {
+             mSession.setActive(true);
+         }
 
-            if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
+         if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
+             updateMetadata();
+             mPlayback.play(mPlayingQueue.get(mCurrentIndexOnQueue));
+         }
+     }
 
-                // 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);
-
-            if (mState == PlaybackState.STATE_PAUSED) {
-                mState = PlaybackState.STATE_STOPPED;
-            }
-
-            // 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_STOPPED;
-                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_STOPPED;
-                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);
-
-            if (mState == PlaybackState.STATE_PAUSED) {
-                mState = PlaybackState.STATE_STOPPED;
-            }
-
-            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();
-            }
-        }
-    }
-
-
-
-    /*
-     * 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
-    }
-
-
-
-
-    /**
-     * 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();
-    }
-
-
-
-    /**
-     * Handle a request to play music
-     */
-    private void handlePlayRequest() {
-        LogHelper.d(TAG, "handlePlayRequest: mState=" + mState);
-
-        mDelayedStopHandler.removeCallbacksAndMessages(null);
-        if (!mServiceStarted) {
-            LogHelper.v(TAG, "Starting service");
-            // The MusicService needs to keep running even after the calling MediaBrowser
-            // is disconnected. Call startService(Intent) and then stopSelf(..) when we no longer
-            // need to play media.
-            startService(new Intent(getApplicationContext(), MusicService.class));
-            mServiceStarted = true;
-        }
-
-        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);
-
-        mMediaNotificationManager.stopNotification();
-
-        // service is no longer necessary. Will be started again if needed.
-        stopSelf();
-        mServiceStarted = false;
-    }
-
-    /**
-     * 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);
-
-        // reset the delayed stop handler.
-        mDelayedStopHandler.removeCallbacksAndMessages(null);
-        mDelayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY);
-
-        // 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);
+     /**
+      * Handle a request to pause music
+      */
+     private void handlePauseRequest() {
+         LogHelper.d(TAG, "handlePauseRequest: mState=" + mPlayback.getState());
+         mPlayback.pause();
+         // reset the delayed stop handler.
+         mDelayedStopHandler.removeCallbacksAndMessages(null);
+         mDelayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY);
+     }
 
-            // 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();
-        }
-    }
+     /**
+      * Handle a request to stop music
+      */
+     private void handleStopRequest(String withError) {
+         LogHelper.d(TAG, "handleStopRequest: mState=" + mPlayback.getState() + " error=", withError);
+         mPlayback.stop(true);
+         // reset the delayed stop handler.
+         mDelayedStopHandler.removeCallbacksAndMessages(null);
+         mDelayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY);
 
-    /**
-     * 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);
+         updatePlaybackState(withError);
 
-        mState = PlaybackState.STATE_STOPPED;
-        relaxResources(false); // release everything except MediaPlayer
+         // service is no longer necessary. Will be started again if needed.
+         stopSelf();
+         mServiceStarted = false;
+     }
 
-        try {
-            createMediaPlayerIfNeeded();
+     private void updateMetadata() {
+         if (!QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
+             LogHelper.e(TAG, "Can't retrieve current metadata.");
+             updatePlaybackState(getResources().getString(R.string.error_no_metadata));
+             return;
+         }
+         MediaSession.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue);
+         String musicId = MediaIDHelper.extractMusicIDFromMediaID(
+                 queueItem.getDescription().getMediaId());
+         MediaMetadata track = mMusicProvider.getMusic(musicId);
+         final String trackId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
+         if (!musicId.equals(trackId)) {
+             IllegalStateException e = new IllegalStateException("track ID should match musicId.");
+             LogHelper.e(TAG, "track ID should match musicId.",
+                 " musicId=", musicId, " trackId=", trackId,
+                 " mediaId from queueItem=", queueItem.getDescription().getMediaId(),
+                 " title from queueItem=", queueItem.getDescription().getTitle(),
+                 " mediaId from track=", track.getDescription().getMediaId(),
+                 " title from track=", track.getDescription().getTitle(),
+                 " source.hashcode from track=", track.getString(
+                     MusicProvider.CUSTOM_METADATA_TRACK_SOURCE).hashCode(),
+                 e);
+             throw e;
+         }
+         LogHelper.d(TAG, "Updating metadata for MusicID= " + musicId);
+         mSession.setMetadata(track);
 
-            mState = PlaybackState.STATE_BUFFERING;
+         // Set the proper album artwork on the media session, so it can be shown in the
+         // locked screen and in other places.
+         if (track.getDescription().getIconBitmap() == null &&
+                 track.getDescription().getIconUri() != null) {
+             String albumUri = track.getDescription().getIconUri().toString();
+             AlbumArtCache.getInstance().fetch(albumUri, new AlbumArtCache.FetchListener() {
+                 @Override
+                 public void onFetched(String artUrl, Bitmap bitmap, Bitmap icon) {
+                     MediaSession.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue);
+                     MediaMetadata track = mMusicProvider.getMusic(trackId);
+                     track = new MediaMetadata.Builder(track)
 
-            mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
-            mMediaPlayer.setDataSource(source);
+                         // set high resolution bitmap in METADATA_KEY_ALBUM_ART. This is used, for
+                         // example, on the lockscreen background when the media session is active.
+                         .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bitmap)
 
-            // 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();
+                         // set small version of the album art in the DISPLAY_ICON. This is used on
+                         // the MediaDescription and thus it should be small to be serialized if
+                         // necessary..
+                         .putBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON, icon)
 
-            // 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();
+                         .build();
 
-            updatePlaybackState(null);
-            updateMetadata();
+                     mMusicProvider.updateMusic(trackId, track);
 
-        } catch (IOException ex) {
-            LogHelper.e(TAG, ex, "IOException playing song");
-            updatePlaybackState(ex.getMessage());
-        }
-    }
+                     // If we are still playing the same music
+                     String currentPlayingId = MediaIDHelper.extractMusicIDFromMediaID(
+                         queueItem.getDescription().getMediaId());
+                     if (trackId.equals(currentPlayingId)) {
+                         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, playback state=" + mPlayback.getState());
+         long position = PlaybackState.PLAYBACK_POSITION_UNKNOWN;
+         if (mPlayback != null && mPlayback.isConnected()) {
+             position = mPlayback.getCurrentStreamPosition();
+         }
 
+         PlaybackState.Builder stateBuilder = new PlaybackState.Builder()
+                 .setActions(getAvailableActions());
 
-    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);
-    }
+         setCustomAction(stateBuilder);
+         int state = mPlayback.getState();
 
+         // 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);
+             state = PlaybackState.STATE_ERROR;
+         }
+         stateBuilder.setState(state, position, 1.0f, SystemClock.elapsedRealtime());
 
-    /**
-     * 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) {
+         // Set the activeQueueItemId if the current index is valid.
+         if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
+             MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue);
+             stateBuilder.setActiveQueueItemId(item.getQueueId());
+         }
 
-        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());
+         mSession.setPlaybackState(stateBuilder.build());
 
-        setCustomAction(stateBuilder);
+         if (state == PlaybackState.STATE_PLAYING || state == PlaybackState.STATE_PAUSED) {
+             mMediaNotificationManager.startNotification();
+         }
+     }
 
-        // 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());
+     private void setCustomAction(PlaybackState.Builder stateBuilder) {
+         MediaMetadata currentMusic = getCurrentPlayingMusic();
+         if (currentMusic != null) {
+             // Set appropriate "Favorite" icon on Custom action:
+             String musicId = currentMusic.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
+             int favoriteIcon = R.drawable.ic_star_off;
+             if (mMusicProvider.isFavorite(musicId)) {
+                 favoriteIcon = R.drawable.ic_star_on;
+             }
+             LogHelper.d(TAG, "updatePlaybackState, setting Favorite custom action of music ",
+                     musicId, " current favorite=", mMusicProvider.isFavorite(musicId));
+             stateBuilder.addCustomAction(CUSTOM_ACTION_THUMBS_UP, getString(R.string.favorite),
+                 favoriteIcon);
+         }
+     }
 
-        // Set the activeQueueItemId if the current index is valid.
-        if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
-            MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue);
-            stateBuilder.setActiveQueueItemId(item.getQueueId());
-        }
+     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 (mPlayback.isPlaying()) {
+             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;
+     }
 
-        mSession.setPlaybackState(stateBuilder.build());
+     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(
+                         MediaIDHelper.extractMusicIDFromMediaID(item.getDescription().getMediaId()));
+             }
+         }
+         return null;
+     }
 
-        if (mState == PlaybackState.STATE_PLAYING || mState == PlaybackState.STATE_PAUSED) {
-            mMediaNotificationManager.startNotification();
-        }
-    }
+     /**
+      * Implementation of the Playback.Callback interface
+      */
+     @Override
+     public void onCompletion() {
+         // 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);
+         }
+     }
 
-    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 = R.drawable.ic_star_off;
-            if (mMusicProvider.isFavorite(mediaId)) {
-                favoriteIcon = R.drawable.ic_star_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);
-        }
-    }
+     @Override
+     public void onPlaybackStatusChanged(int state) {
+         updatePlaybackState(null);
+     }
 
-    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;
-    }
+     @Override
+     public void onError(String error) {
+         updatePlaybackState(error);
+     }
 
-    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;
-    }
+     /**
+      * A simple handler that stops the service if playback is not active (playing)
+      */
+     private static class DelayedStopHandler extends Handler {
+         private final WeakReference<MusicService> mWeakReference;
 
-    /**
-     * 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;
-            }
-        }
-    }
+         private DelayedStopHandler(MusicService service) {
+             mWeakReference = new WeakReference<>(service);
+         }
 
-    /**
-     * 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;
-            }
-        }
-    }
-}
+         @Override
+         public void handleMessage(Message msg) {
+             MusicService service = mWeakReference.get();
+             if (service != null && service.mPlayback != null) {
+                 if (service.mPlayback.isPlaying()) {
+                     LogHelper.d(TAG, "Ignoring delayed stop since the media player is in use.");
+                     return;
+                 }
+                 LogHelper.d(TAG, "Stopping service with delay handler.");
+                 service.stopSelf();
+                 service.mServiceStarted = false;
+             }
+         }
+     }
+ }
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/PackageValidator.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/PackageValidator.java
index 9e40894..c2bce54 100644
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/PackageValidator.java
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/PackageValidator.java
@@ -15,205 +15,145 @@
  */
 package com.example.android.mediabrowserservice;
 
-import java.io.UnsupportedEncodingException;
-import java.util.Arrays;
-
 import android.content.Context;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
-import android.os.Build;
+import android.content.res.XmlResourceParser;
 import android.os.Process;
 import android.util.Base64;
-import android.util.Log;
 
 import com.example.android.mediabrowserservice.utils.LogHelper;
 
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
 /**
- * Validates that the calling package is authorized to use this
+ * Validates that the calling package is authorized to browse a
  * {@link android.service.media.MediaBrowserService}.
+ *
+ * The list of allowed signing certificates and their corresponding package names is defined in
+ * res/xml/allowed_media_browser_callers.xml.
  */
 public class PackageValidator {
-
-    private static final String TAG = LogHelper.makeLogTag(PackageValidator.class.getSimpleName());
-
-    // Replace with your package whitelist
-    static final byte[][] VALID_PUBLIC_SIGNATURES = new byte[][]{
-        // Android Auto release public key
-        extractKey(
-        "\060\202\003\275\060\202\002\245\240\003\002\001\002\002\011\000\307\217\236\113" +
-        "\223\101\060\006\060\015\006\011\052\206\110\206\367\015\001\001\005\005\000\060" +
-        "\165\061\013\060\011\006\003\125\004\006\023\002\125\123\061\023\060\021\006\003" +
-        "\125\004\010\014\012\103\141\154\151\146\157\162\156\151\141\061\026\060\024\006" +
-        "\003\125\004\007\014\015\115\157\165\156\164\141\151\156\040\126\151\145\167\061" +
-        "\024\060\022\006\003\125\004\012\014\013\107\157\157\147\154\145\040\111\156\143" +
-        "\056\061\020\060\016\006\003\125\004\013\014\007\101\156\144\162\157\151\144\061" +
-        "\021\060\017\006\003\125\004\003\014\010\147\145\141\162\150\145\141\144\060\036" +
-        "\027\015\061\064\060\065\062\067\062\063\060\065\063\064\132\027\015\064\061\061" +
-        "\060\061\062\062\063\060\065\063\064\132\060\165\061\013\060\011\006\003\125\004" +
-        "\006\023\002\125\123\061\023\060\021\006\003\125\004\010\014\012\103\141\154\151" +
-        "\146\157\162\156\151\141\061\026\060\024\006\003\125\004\007\014\015\115\157\165" +
-        "\156\164\141\151\156\040\126\151\145\167\061\024\060\022\006\003\125\004\012\014" +
-        "\013\107\157\157\147\154\145\040\111\156\143\056\061\020\060\016\006\003\125\004" +
-        "\013\014\007\101\156\144\162\157\151\144\061\021\060\017\006\003\125\004\003\014" +
-        "\010\147\145\141\162\150\145\141\144\060\202\001\042\060\015\006\011\052\206\110" +
-        "\206\367\015\001\001\001\005\000\003\202\001\017\000\060\202\001\012\002\202\001" +
-        "\001\000\323\235\027\016\103\110\261\124\114\137\154\023\275\132\145\244\053\270" +
-        "\072\331\362\064\255\257\344\036\317\113\340\340\202\141\366\312\346\142\302\224" +
-        "\356\255\322\203\103\324\175\123\074\107\365\116\045\260\057\246\043\025\344\210" +
-        "\026\012\041\143\125\200\313\142\116\014\144\023\056\334\201\153\335\140\170\015" +
-        "\142\221\156\360\214\131\051\200\362\135\353\076\323\152\137\276\233\272\334\302" +
-        "\001\017\363\347\275\121\142\246\215\150\122\266\337\172\330\376\232\272\004\246" +
-        "\071\300\357\130\024\113\103\244\370\176\227\131\153\046\157\314\105\035\005\114" +
-        "\241\225\204\043\073\024\047\151\341\233\301\034\234\371\000\075\363\131\000\157" +
-        "\276\134\263\321\072\204\120\011\253\060\311\213\035\343\142\156\140\003\367\013" +
-        "\006\156\204\067\024\154\305\246\223\272\301\213\320\125\103\310\046\222\266\360" +
-        "\252\217\170\003\272\222\264\265\051\334\334\202\232\122\222\130\166\231\323\224" +
-        "\254\244\103\360\261\367\055\221\255\050\134\156\133\206\004\372\353\261\014\013" +
-        "\064\076\142\301\115\326\202\121\057\264\052\372\143\020\214\122\154\337\002\003" +
-        "\001\000\001\243\120\060\116\060\035\006\003\125\035\016\004\026\004\024\032\360" +
-        "\137\140\327\256\350\224\211\122\162\131\012\046\201\032\311\327\316\333\060\037" +
-        "\006\003\125\035\043\004\030\060\026\200\024\032\360\137\140\327\256\350\224\211" +
-        "\122\162\131\012\046\201\032\311\327\316\333\060\014\006\003\125\035\023\004\005" +
-        "\060\003\001\001\377\060\015\006\011\052\206\110\206\367\015\001\001\005\005\000" +
-        "\003\202\001\001\000\224\153\003\143\101\017\273\163\101\110\176\144\352\054\077" +
-        "\300\230\175\173\174\114\301\055\173\022\262\206\226\034\226\242\014\111\063\062" +
-        "\343\000\336\240\321\240\217\037\020\170\320\204\002\373\312\200\227\344\113\355" +
-        "\124\061\352\214\155\265\375\046\337\134\224\031\003\334\065\206\355\330\054\101" +
-        "\114\040\053\363\316\150\054\256\155\331\060\042\346\324\063\205\336\231\021\210" +
-        "\241\131\045\026\121\337\327\360\024\021\242\354\133\242\313\075\101\260\100\376" +
-        "\042\061\320\352\103\153\030\200\162\256\302\157\256\323\205\345\331\017\021\256" +
-        "\103\307\346\035\206\313\307\316\051\022\371\267\015\003\201\374\262\014\222\112" +
-        "\120\111\361\002\325\377\250\077\134\301\336\352\317\123\367\122\274\100\377\054" +
-        "\050\016\166\272\161\147\227\142\355\054\022\312\347\276\126\257\323\145\014\267" +
-        "\342\323\362\200\114\303\331\337\041\026\130\177\311\370\126\220\310\263\071\342" +
-        "\027\161\254\225\001\007\115\237\234\351\006\113\232\313\133\044\030\350\320\103" +
-        "\231\023\154\067\003\316\050\016\331\035\253\252\176\207\011\337\145\345\235\026" +
-        "\041"),
-
-        // Android Auto debug public key
-        extractKey(
-        "\060\202\003\275\060\202\002\245\240\003\002\001\002\002\011\000\347\344\006\360" +
-        "\327\303\226\363\060\015\006\011\052\206\110\206\367\015\001\001\005\005\000\060" +
-        "\165\061\013\060\011\006\003\125\004\006\023\002\125\123\061\023\060\021\006\003" +
-        "\125\004\010\014\012\103\141\154\151\146\157\162\156\151\141\061\026\060\024\006" +
-        "\003\125\004\007\014\015\115\157\165\156\164\141\151\156\040\126\151\145\167\061" +
-        "\024\060\022\006\003\125\004\012\014\013\107\157\157\147\154\145\040\111\156\143" +
-        "\056\061\020\060\016\006\003\125\004\013\014\007\101\156\144\162\157\151\144\061" +
-        "\021\060\017\006\003\125\004\003\014\010\147\145\141\162\150\145\141\144\060\036" +
-        "\027\015\061\064\060\065\062\067\062\063\060\062\065\061\132\027\015\064\061\061" +
-        "\060\061\062\062\063\060\062\065\061\132\060\165\061\013\060\011\006\003\125\004" +
-        "\006\023\002\125\123\061\023\060\021\006\003\125\004\010\014\012\103\141\154\151" +
-        "\146\157\162\156\151\141\061\026\060\024\006\003\125\004\007\014\015\115\157\165" +
-        "\156\164\141\151\156\040\126\151\145\167\061\024\060\022\006\003\125\004\012\014" +
-        "\013\107\157\157\147\154\145\040\111\156\143\056\061\020\060\016\006\003\125\004" +
-        "\013\014\007\101\156\144\162\157\151\144\061\021\060\017\006\003\125\004\003\014" +
-        "\010\147\145\141\162\150\145\141\144\060\202\001\042\060\015\006\011\052\206\110" +
-        "\206\367\015\001\001\001\005\000\003\202\001\017\000\060\202\001\012\002\202\001" +
-        "\001\000\242\356\360\300\022\205\313\071\352\245\032\336\264\235\304\126\236\171" +
-        "\375\212\364\343\320\040\347\011\106\276\260\247\214\203\374\016\263\053\123\353" +
-        "\044\174\247\265\016\154\051\260\263\155\236\030\142\064\177\211\323\115\013\242" +
-        "\115\341\163\310\335\130\247\212\072\212\163\050\140\315\274\277\307\276\164\273" +
-        "\321\234\244\333\250\043\366\073\114\060\174\375\331\246\135\246\154\003\353\261" +
-        "\115\231\071\106\330\121\021\257\344\360\060\076\132\201\243\347\260\124\166\316" +
-        "\126\272\272\005\057\034\154\363\353\226\003\306\220\231\261\017\323\243\014\203" +
-        "\056\174\140\061\250\057\206\364\276\071\354\167\312\035\205\067\272\111\177\004" +
-        "\264\334\247\106\166\105\217\154\272\237\364\127\246\323\333\071\216\067\231\133" +
-        "\363\267\106\011\312\241\023\310\047\204\013\053\275\036\176\060\031\250\234\201" +
-        "\031\300\331\311\003\060\072\317\274\034\211\047\255\247\374\371\304\131\044\074" +
-        "\352\073\036\353\266\331\174\063\162\206\007\141\005\226\064\351\353\361\162\304" +
-        "\222\347\002\216\220\225\171\373\032\266\032\225\062\064\310\265\075\165\002\003" +
-        "\001\000\001\243\120\060\116\060\035\006\003\125\035\016\004\026\004\024\365\003" +
-        "\311\347\022\104\014\017\014\015\003\053\217\110\146\333\360\066\005\031\060\037" +
-        "\006\003\125\035\043\004\030\060\026\200\024\365\003\311\347\022\104\014\017\014" +
-        "\015\003\053\217\110\146\333\360\066\005\031\060\014\006\003\125\035\023\004\005" +
-        "\060\003\001\001\377\060\015\006\011\052\206\110\206\367\015\001\001\005\005\000" +
-        "\003\202\001\001\000\015\312\371\207\121\121\360\212\146\067\210\122\261\100\075" +
-        "\112\160\220\127\045\332\324\144\041\316\224\040\105\261\176\236\231\040\072\175" +
-        "\214\171\272\174\155\335\274\126\227\340\242\200\366\070\023\120\134\045\034\146" +
-        "\111\373\245\150\376\372\353\175\036\023\233\035\126\225\344\123\140\322\227\103" +
-        "\250\271\332\365\006\175\143\212\022\371\232\342\214\256\364\135\237\304\216\126" +
-        "\024\036\370\156\322\222\043\144\006\303\360\051\202\026\132\060\111\036\171\250" +
-        "\044\243\063\230\222\337\262\331\007\175\222\062\275\101\006\046\053\064\013\347" +
-        "\160\250\330\101\122\274\162\324\321\316\032\115\101\003\301\201\160\100\367\305" +
-        "\345\371\335\103\077\055\064\045\144\056\027\113\054\232\022\234\046\353\337\164" +
-        "\111\305\027\261\357\153\034\377\200\044\075\237\066\253\100\215\302\044\037\035" +
-        "\071\165\160\027\311\234\310\064\101\317\202\121\371\200\351\136\216\201\017\347" +
-        "\306\267\136\150\277\354\346\250\057\061\151\077\117\327\362\140\240\065\342\062" +
-        "\034\277\352\274\040\166\057\126\304\367\374\231\276\323\234\020\276\012\113\027" +
-        "\320"),
-    };
+    private static final String TAG = LogHelper.makeLogTag(PackageValidator.class);
 
     /**
-     * Disallow instantiation of this helper class.
+     * Map allowed callers' certificate keys to the expected caller information.
+     *
      */
-    private PackageValidator() {}
+    private final Map<String, ArrayList<CallerInfo>> mValidCertificates;
 
-    /**
-     * Throws when the caller is not authorized to get data from this MediaBrowserService
-     */
-    public static void checkCallerAllowed(Context context, String callingPackage, int callingUid) {
-        if (!isCallerAllowed(context, callingPackage, callingUid)) {
-            throw new SecurityException("signature check failed.");
+    public PackageValidator(Context ctx) {
+        mValidCertificates = readValidCertificates(ctx.getResources().getXml(
+            R.xml.allowed_media_browser_callers));
+    }
+
+    private Map<String, ArrayList<CallerInfo>> readValidCertificates(XmlResourceParser parser) {
+        HashMap<String, ArrayList<CallerInfo>> validCertificates = new HashMap<>();
+        try {
+            int eventType = parser.next();
+            while (eventType != XmlResourceParser.END_DOCUMENT) {
+                if (eventType == XmlResourceParser.START_TAG
+                        && parser.getName().equals("signing_certificate")) {
+
+                    String name = parser.getAttributeValue(null, "name");
+                    String packageName = parser.getAttributeValue(null, "package");
+                    boolean isRelease = parser.getAttributeBooleanValue(null, "release", false);
+                    String certificate = parser.nextText().replaceAll("\\s|\\n", "");
+
+                    CallerInfo info = new CallerInfo(name, packageName, isRelease, certificate);
+
+                    ArrayList<CallerInfo> infos = validCertificates.get(certificate);
+                    if (infos == null) {
+                        infos = new ArrayList<>();
+                        validCertificates.put(certificate, infos);
+                    }
+                    LogHelper.v(TAG, "Adding allowed caller: ", info.name,
+                        " package=", info.packageName, " release=", info.release,
+                        " certificate=", certificate);
+                    infos.add(info);
+                }
+                eventType = parser.next();
+            }
+        } catch (XmlPullParserException | IOException e) {
+            LogHelper.e(TAG, e, "Could not read allowed callers from XML.");
         }
+        return validCertificates;
     }
 
     /**
      * @return false if the caller is not authorized to get data from this MediaBrowserService
      */
-    public static boolean isCallerAllowed(Context context, String callingPackage, int callingUid) {
-        // Always allow calls from the framework or development environment.
+    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+    public boolean isCallerAllowed(Context context, String callingPackage, int callingUid) {
+        // Always allow calls from the framework, self app or development environment.
         if (Process.SYSTEM_UID == callingUid || Process.myUid() == callingUid) {
             return true;
         }
-        if (BuildConfig.DEBUG) {
-            // When your app is built in debug mode, any app is allowed to connect to it and browse
-            // its media library. If you want to test the behavior of your app when it gets
-            // released, either build a release version or remove this clause.
-            Log.i(TAG, "Allowing caller '"+callingPackage+" because app was built in debug mode.");
-            return true;
-        }
+        PackageManager packageManager = context.getPackageManager();
         PackageInfo packageInfo;
-        final PackageManager packageManager = context.getPackageManager();
         try {
             packageInfo = packageManager.getPackageInfo(
                     callingPackage, PackageManager.GET_SIGNATURES);
-        } catch (PackageManager.NameNotFoundException ignored) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "Package manager can't find package " + callingPackage
-                        + ", defaulting to false");
+        } catch (PackageManager.NameNotFoundException e) {
+            LogHelper.w(TAG, e, "Package manager can't find package: ", callingPackage);
+            return false;
+        }
+        if (packageInfo.signatures.length != 1) {
+            LogHelper.w(TAG, "Caller has more than one signature certificate!");
+            return false;
+        }
+        String signature = Base64.encodeToString(
+            packageInfo.signatures[0].toByteArray(), Base64.NO_WRAP);
+
+        // Test for known signatures:
+        ArrayList<CallerInfo> validCallers = mValidCertificates.get(signature);
+        if (validCallers == null) {
+            LogHelper.v(TAG, "Signature for caller ", callingPackage, " is not valid: \n"
+                , signature);
+            if (mValidCertificates.isEmpty()) {
+                LogHelper.w(TAG, "The list of valid certificates is empty. Either your file ",
+                        "res/xml/allowed_media_browser_callers.xml is empty or there was an error ",
+                        "while reading it. Check previous log messages.");
             }
             return false;
         }
-        if (packageInfo == null) {
-            Log.w(TAG, "Package manager can't find package: " + callingPackage);
-            return false;
-        }
 
-        if (packageInfo.signatures.length != 1) {
-            Log.w(TAG, "Package has more than one signature.");
-            return false;
-        }
-        final byte[] signature = packageInfo.signatures[0].toByteArray();
-
-        for (int i = 0; i < VALID_PUBLIC_SIGNATURES.length; i++) {
-            byte[] validSignature = VALID_PUBLIC_SIGNATURES[i];
-            if (Arrays.equals(validSignature, signature)) {
+        // Check if the package name is valid for the certificate:
+        StringBuffer expectedPackages = new StringBuffer();
+        for (CallerInfo info: validCallers) {
+            if (callingPackage.equals(info.packageName)) {
+                LogHelper.v(TAG, "Valid caller: ", info.name, "  package=", info.packageName,
+                    " release=", info.release);
                 return true;
             }
+            expectedPackages.append(info.packageName).append(' ');
         }
 
-        if (Log.isLoggable(TAG, Log.VERBOSE)) {
-            Log.v(TAG, "Signature not valid.  Found: \n" +
-                    Base64.encodeToString(signature, 0));
-        }
+        LogHelper.i(TAG, "Caller has a valid certificate, but its package doesn't match any ",
+            "expected package for the given certificate. Caller's package is ", callingPackage,
+            ". Expected packages as defined in res/xml/allowed_media_browser_callers.xml are (",
+            expectedPackages, "). This caller's certificate is: \n", signature);
+
         return false;
     }
 
-    private static byte[] extractKey(String keyString) {
-        try {
-            return keyString.getBytes("ISO-8859-1");
-        } catch (UnsupportedEncodingException e) {
-            throw new AssertionError(e);
+    private final static class CallerInfo {
+        final String name;
+        final String packageName;
+        final boolean release;
+        final String signingCertificate;
+
+        public CallerInfo(String name, String packageName, boolean release,
+                          String signingCertificate) {
+            this.name = name;
+            this.packageName = packageName;
+            this.release = release;
+            this.signingCertificate = signingCertificate;
         }
     }
 }
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/Playback.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/Playback.java
new file mode 100644
index 0000000..fb3ff28
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/Playback.java
@@ -0,0 +1,489 @@
+/*
+ * 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.mediabrowserservice;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.media.MediaMetadata;
+import android.media.MediaPlayer;
+import android.media.session.PlaybackState;
+import android.net.wifi.WifiManager;
+import android.os.PowerManager;
+import android.text.TextUtils;
+
+import com.example.android.mediabrowserservice.model.MusicProvider;
+import com.example.android.mediabrowserservice.utils.LogHelper;
+import com.example.android.mediabrowserservice.utils.MediaIDHelper;
+
+import java.io.IOException;
+
+import static android.media.MediaPlayer.OnCompletionListener;
+import static android.media.MediaPlayer.OnErrorListener;
+import static android.media.MediaPlayer.OnPreparedListener;
+import static android.media.MediaPlayer.OnSeekCompleteListener;
+import static android.media.session.MediaSession.QueueItem;
+
+/**
+ * A class that implements local media playback using {@link android.media.MediaPlayer}
+ */
+public class Playback implements AudioManager.OnAudioFocusChangeListener,
+        OnCompletionListener, OnErrorListener, OnPreparedListener, OnSeekCompleteListener {
+
+    private static final String TAG = LogHelper.makeLogTag(Playback.class);
+
+    // 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;
+
+    // we don't have audio focus, and can't duck (play at a low volume)
+    private static final int AUDIO_NO_FOCUS_NO_DUCK = 0;
+    // we don't have focus, but can duck (play at a low volume)
+    private static final int AUDIO_NO_FOCUS_CAN_DUCK = 1;
+    // we have full audio focus
+    private static final int AUDIO_FOCUSED  = 2;
+
+    private final MusicService mService;
+    private final WifiManager.WifiLock mWifiLock;
+    private int mState;
+    private boolean mPlayOnFocusGain;
+    private Callback mCallback;
+    private MusicProvider mMusicProvider;
+    private volatile boolean mAudioNoisyReceiverRegistered;
+    private volatile int mCurrentPosition;
+    private volatile String mCurrentMediaId;
+
+    // Type of audio focus we have:
+    private int mAudioFocus = AUDIO_NO_FOCUS_NO_DUCK;
+    private AudioManager mAudioManager;
+    private MediaPlayer mMediaPlayer;
+
+    private IntentFilter mAudioNoisyIntentFilter =
+            new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
+
+    private BroadcastReceiver mAudioNoisyReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
+                LogHelper.d(TAG, "Headphones disconnected.");
+                if (isPlaying()) {
+                    Intent i = new Intent(context, MusicService.class);
+                    i.setAction(MusicService.ACTION_CMD);
+                    i.putExtra(MusicService.CMD_NAME, MusicService.CMD_PAUSE);
+                    mService.startService(i);
+                }
+            }
+        }
+    };
+
+    public Playback(MusicService service, MusicProvider musicProvider) {
+        this.mService = service;
+        this.mMusicProvider = musicProvider;
+        this.mAudioManager = (AudioManager) service.getSystemService(Context.AUDIO_SERVICE);
+        // Create the Wifi lock (this does not acquire the lock, this just creates it)
+        this.mWifiLock = ((WifiManager) service.getSystemService(Context.WIFI_SERVICE))
+                .createWifiLock(WifiManager.WIFI_MODE_FULL, "sample_lock");
+    }
+
+    public void start() {
+    }
+
+    public void stop(boolean notifyListeners) {
+        mState = PlaybackState.STATE_STOPPED;
+        if (notifyListeners && mCallback != null) {
+            mCallback.onPlaybackStatusChanged(mState);
+        }
+        mCurrentPosition = getCurrentStreamPosition();
+        // Give up Audio focus
+        giveUpAudioFocus();
+        unregisterAudioNoisyReceiver();
+        // Relax all resources
+        relaxResources(true);
+        if (mWifiLock.isHeld()) {
+            mWifiLock.release();
+        }
+    }
+
+    public void setState(int state) {
+        this.mState = state;
+    }
+
+    public int getState() {
+        return mState;
+    }
+
+    public boolean isConnected() {
+        return true;
+    }
+
+    public boolean isPlaying() {
+        return mPlayOnFocusGain || (mMediaPlayer != null && mMediaPlayer.isPlaying());
+    }
+
+    public int getCurrentStreamPosition() {
+        return mMediaPlayer != null ?
+                mMediaPlayer.getCurrentPosition() : mCurrentPosition;
+    }
+
+    public void play(QueueItem item) {
+        mPlayOnFocusGain = true;
+        tryToGetAudioFocus();
+        registerAudioNoisyReceiver();
+        String mediaId = item.getDescription().getMediaId();
+        boolean mediaHasChanged = !TextUtils.equals(mediaId, mCurrentMediaId);
+        if (mediaHasChanged) {
+            mCurrentPosition = 0;
+            mCurrentMediaId = mediaId;
+        }
+
+        if (mState == PlaybackState.STATE_PAUSED && !mediaHasChanged && mMediaPlayer != null) {
+            configMediaPlayerState();
+        } else {
+            mState = PlaybackState.STATE_STOPPED;
+            relaxResources(false); // release everything except MediaPlayer
+            MediaMetadata track = mMusicProvider.getMusic(
+                    MediaIDHelper.extractMusicIDFromMediaID(item.getDescription().getMediaId()));
+
+            String source = track.getString(MusicProvider.CUSTOM_METADATA_TRACK_SOURCE);
+
+            try {
+                createMediaPlayerIfNeeded();
+
+                mState = PlaybackState.STATE_BUFFERING;
+
+                mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+                mMediaPlayer.setDataSource(source);
+
+                // 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();
+
+                if (mCallback != null) {
+                    mCallback.onPlaybackStatusChanged(mState);
+                }
+
+            } catch (IOException ex) {
+                LogHelper.e(TAG, ex, "Exception playing song");
+                if (mCallback != null) {
+                    mCallback.onError(ex.getMessage());
+                }
+            }
+        }
+    }
+
+    public void pause() {
+        if (mState == PlaybackState.STATE_PLAYING) {
+            // Pause media player and cancel the 'foreground service' state.
+            if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
+                mMediaPlayer.pause();
+                mCurrentPosition = mMediaPlayer.getCurrentPosition();
+            }
+            // while paused, retain the MediaPlayer but give up audio focus
+            relaxResources(false);
+            giveUpAudioFocus();
+        }
+        mState = PlaybackState.STATE_PAUSED;
+        if (mCallback != null) {
+            mCallback.onPlaybackStatusChanged(mState);
+        }
+        unregisterAudioNoisyReceiver();
+    }
+
+    public void seekTo(int position) {
+        LogHelper.d(TAG, "seekTo called with ", position);
+
+        if (mMediaPlayer == null) {
+            // If we do not have a current media player, simply update the current position
+            mCurrentPosition = position;
+        } else {
+            if (mMediaPlayer.isPlaying()) {
+                mState = PlaybackState.STATE_BUFFERING;
+            }
+            mMediaPlayer.seekTo(position);
+            if (mCallback != null) {
+                mCallback.onPlaybackStatusChanged(mState);
+            }
+        }
+    }
+
+    public void setCallback(Callback callback) {
+        this.mCallback = callback;
+    }
+
+    /**
+     * Try to get the system audio focus.
+     */
+    private void tryToGetAudioFocus() {
+        LogHelper.d(TAG, "tryToGetAudioFocus");
+        if (mAudioFocus != AUDIO_FOCUSED) {
+            int result = mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
+                    AudioManager.AUDIOFOCUS_GAIN);
+            if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+                mAudioFocus = AUDIO_FOCUSED;
+            }
+        }
+    }
+
+    /**
+     * Give up the audio focus.
+     */
+    private void giveUpAudioFocus() {
+        LogHelper.d(TAG, "giveUpAudioFocus");
+        if (mAudioFocus == AUDIO_FOCUSED) {
+            if (mAudioManager.abandonAudioFocus(this) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+                mAudioFocus = AUDIO_NO_FOCUS_NO_DUCK;
+            }
+        }
+    }
+
+    /**
+     * 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, "configMediaPlayerState. mAudioFocus=", mAudioFocus);
+        if (mAudioFocus == AUDIO_NO_FOCUS_NO_DUCK) {
+            // If we don't have audio focus and can't duck, we have to pause,
+            if (mState == PlaybackState.STATE_PLAYING) {
+                pause();
+            }
+        } else {  // we have audio focus:
+            if (mAudioFocus == AUDIO_NO_FOCUS_CAN_DUCK) {
+                mMediaPlayer.setVolume(VOLUME_DUCK, VOLUME_DUCK); // we'll be relatively quiet
+            } else {
+                if (mMediaPlayer != null) {
+                    mMediaPlayer.setVolume(VOLUME_NORMAL, VOLUME_NORMAL); // we can be loud again
+                } // else do something for remote client.
+            }
+            // If we were playing when we lost focus, we need to resume playing.
+            if (mPlayOnFocusGain) {
+                if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) {
+                    LogHelper.d(TAG,"configMediaPlayerState startMediaPlayer. seeking to ",
+                        mCurrentPosition);
+                    if (mCurrentPosition == mMediaPlayer.getCurrentPosition()) {
+                        mMediaPlayer.start();
+                        mState = PlaybackState.STATE_PLAYING;
+                    } else {
+                        mMediaPlayer.seekTo(mCurrentPosition);
+                        mState = PlaybackState.STATE_BUFFERING;
+                    }
+                }
+                mPlayOnFocusGain = false;
+            }
+        }
+        if (mCallback != null) {
+            mCallback.onPlaybackStatusChanged(mState);
+        }
+    }
+
+    /**
+     * Called by AudioManager on audio focus changes.
+     * Implementation of {@link android.media.AudioManager.OnAudioFocusChangeListener}
+     */
+    @Override
+    public void onAudioFocusChange(int focusChange) {
+        LogHelper.d(TAG, "onAudioFocusChange. focusChange=", focusChange);
+        if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
+            // We have gained focus:
+            mAudioFocus = AUDIO_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 ? AUDIO_NO_FOCUS_CAN_DUCK : AUDIO_NO_FOCUS_NO_DUCK;
+
+            // 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();
+    }
+
+    /**
+     * Called when MediaPlayer has completed a seek
+     *
+     * @see android.media.MediaPlayer.OnSeekCompleteListener
+     */
+    @Override
+    public void onSeekComplete(MediaPlayer mp) {
+        LogHelper.d(TAG, "onSeekComplete from MediaPlayer:", mp.getCurrentPosition());
+        mCurrentPosition = mp.getCurrentPosition();
+        if (mState == PlaybackState.STATE_BUFFERING) {
+            mMediaPlayer.start();
+            mState = PlaybackState.STATE_PLAYING;
+        }
+        if (mCallback != null) {
+            mCallback.onPlaybackStatusChanged(mState);
+        }
+    }
+
+    /**
+     * 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 (mCallback != null) {
+            mCallback.onCompletion();
+        }
+    }
+
+    /**
+     * 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);
+        if (mCallback != null) {
+            mCallback.onError("MediaPlayer error " + what + " (" + extra + ")");
+        }
+        return true; // true indicates we handled the error
+    }
+
+    /**
+     * 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(mService.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);
+            mMediaPlayer.setOnSeekCompleteListener(this);
+        } else {
+            mMediaPlayer.reset();
+        }
+    }
+
+    /**
+     * 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);
+
+        mService.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();
+        }
+    }
+
+    private void registerAudioNoisyReceiver() {
+        if (!mAudioNoisyReceiverRegistered) {
+            mService.registerReceiver(mAudioNoisyReceiver, mAudioNoisyIntentFilter);
+            mAudioNoisyReceiverRegistered = true;
+        }
+    }
+
+    private void unregisterAudioNoisyReceiver() {
+        if (mAudioNoisyReceiverRegistered) {
+            mService.unregisterReceiver(mAudioNoisyReceiver);
+            mAudioNoisyReceiverRegistered = false;
+        }
+    }
+
+    interface Callback {
+        /**
+         * On current music completed.
+         */
+        void onCompletion();
+        /**
+         * on Playback status changed
+         * Implementations can use this callback to update
+         * playback state on the media sessions.
+         */
+        void onPlaybackStatusChanged(int state);
+
+        /**
+         * @param error to be added to the PlaybackState
+         */
+        void onError(String error);
+
+    }
+
+}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/model/MusicProvider.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/model/MusicProvider.java
index 6b6a64e..b56bf2a 100644
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/model/MusicProvider.java
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/model/MusicProvider.java
@@ -30,12 +30,14 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.net.URL;
 import java.net.URLConnection;
 import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
+import java.util.Collections;
 import java.util.List;
-import java.util.concurrent.locks.ReentrantLock;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 
 /**
  * Utility class to get a list of MusicTrack's based on a server-side JSON
@@ -43,56 +45,54 @@
  */
 public class MusicProvider {
 
-    private static final String TAG = LogHelper.makeLogTag(MusicProvider.class.getSimpleName());
+    private static final String TAG = LogHelper.makeLogTag(MusicProvider.class);
 
-    private static final String CATALOG_URL = "http://storage.googleapis.com/automotive-media/music.json";
+    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();
+    private static final String JSON_MUSIC = "music";
+    private static final String JSON_TITLE = "title";
+    private static final String JSON_ALBUM = "album";
+    private static final String JSON_ARTIST = "artist";
+    private static final String JSON_GENRE = "genre";
+    private static final String JSON_SOURCE = "source";
+    private static final String JSON_IMAGE = "image";
+    private static final String JSON_TRACK_NUMBER = "trackNumber";
+    private static final String JSON_TOTAL_TRACK_COUNT = "totalTrackCount";
+    private static final String JSON_DURATION = "duration";
 
     // Categorized caches for music track data:
-    private final HashMap<String, List<MediaMetadata>> mMusicListByGenre;
-    private final HashMap<String, MediaMetadata> mMusicListById;
+    private ConcurrentMap<String, List<MediaMetadata>> mMusicListByGenre;
+    private final ConcurrentMap<String, MutableMediaMetadata> mMusicListById;
 
-    private final HashSet<String> mFavoriteTracks;
+    private final Set<String> mFavoriteTracks;
 
     enum State {
-        NON_INITIALIZED, INITIALIZING, INITIALIZED;
+        NON_INITIALIZED, INITIALIZING, INITIALIZED
     }
 
-    private State mCurrentState = State.NON_INITIALIZED;
-
+    private volatile State mCurrentState = State.NON_INITIALIZED;
 
     public interface Callback {
         void onMusicCatalogReady(boolean success);
     }
 
     public MusicProvider() {
-        mMusicListByGenre = new HashMap<>();
-        mMusicListById = new HashMap<>();
-        mFavoriteTracks = new HashSet<>();
+        mMusicListByGenre = new ConcurrentHashMap<>();
+        mMusicListById = new ConcurrentHashMap<>();
+        mFavoriteTracks = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
     }
 
     /**
      * Get an iterator over the list of genres
      *
-     * @return
+     * @return genres
      */
     public Iterable<String> getGenres() {
         if (mCurrentState != State.INITIALIZED) {
-            return new ArrayList<String>(0);
+            return Collections.emptyList();
         }
         return mMusicListByGenre.keySet();
     }
@@ -100,11 +100,10 @@
     /**
      * 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 Collections.emptyList();
         }
         return mMusicListByGenre.get(genre);
     }
@@ -113,32 +112,53 @@
      * 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<>();
+    public Iterable<MediaMetadata> searchMusic(String titleQuery) {
         if (mCurrentState != State.INITIALIZED) {
-            return result;
+            return Collections.emptyList();
         }
+        ArrayList<MediaMetadata> result = new ArrayList<>();
         titleQuery = titleQuery.toLowerCase();
-        for (MediaMetadata track: mMusicListById.values()) {
-            if (track.getString(MediaMetadata.METADATA_KEY_TITLE).toLowerCase()
+        for (MutableMediaMetadata track : mMusicListById.values()) {
+            if (track.metadata.getString(MediaMetadata.METADATA_KEY_TITLE).toLowerCase()
                     .contains(titleQuery)) {
-                result.add(track);
+                result.add(track.metadata);
             }
         }
         return result;
     }
 
-    public MediaMetadata getMusic(String mediaId) {
-        return mMusicListById.get(mediaId);
+    /**
+     * Return the MediaMetadata for the given musicID.
+     *
+     * @param musicId The unique, non-hierarchical music ID.
+     */
+    public MediaMetadata getMusic(String musicId) {
+        return mMusicListById.containsKey(musicId) ? mMusicListById.get(musicId).metadata : null;
     }
 
-    public void setFavorite(String mediaId, boolean favorite) {
+    public synchronized void updateMusic(String musicId, MediaMetadata metadata) {
+        MutableMediaMetadata track = mMusicListById.get(musicId);
+        if (track == null) {
+            return;
+        }
+
+        String oldGenre = track.metadata.getString(MediaMetadata.METADATA_KEY_GENRE);
+        String newGenre = metadata.getString(MediaMetadata.METADATA_KEY_GENRE);
+
+        track.metadata = metadata;
+
+        // if genre has changed, we need to rebuild the list by genre
+        if (!oldGenre.equals(newGenre)) {
+            buildListsByGenre();
+        }
+    }
+
+    public void setFavorite(String musicId, boolean favorite) {
         if (favorite) {
-            mFavoriteTracks.add(mediaId);
+            mFavoriteTracks.add(musicId);
         } else {
-            mFavoriteTracks.remove(mediaId);
+            mFavoriteTracks.remove(musicId);
         }
     }
 
@@ -152,12 +172,10 @@
 
     /**
      * 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
+     * for future reference, keying tracks by musicId and grouping by genre.
      */
-    public void retrieveMedia(final Callback callback) {
-
+    public void retrieveMediaAsync(final Callback callback) {
+        LogHelper.d(TAG, "retrieveMediaAsync called");
         if (mCurrentState == State.INITIALIZED) {
             // Nothing to do, execute callback immediately
             callback.onMusicCatalogReady(true);
@@ -165,44 +183,60 @@
         }
 
         // Asynchronously load the music catalog in a separate thread
-        new AsyncTask() {
+        new AsyncTask<Void, Void, State>() {
             @Override
-            protected Object doInBackground(Object[] objects) {
-                retrieveMediaAsync(callback);
-                return null;
+            protected State doInBackground(Void... params) {
+                retrieveMedia();
+                return mCurrentState;
+            }
+
+            @Override
+            protected void onPostExecute(State current) {
+                if (callback != null) {
+                    callback.onMusicCatalogReady(current == State.INITIALIZED);
+                }
             }
         }.execute();
     }
 
-    private void retrieveMediaAsync(Callback callback) {
-        initializationLock.lock();
+    private synchronized void buildListsByGenre() {
+        ConcurrentMap<String, List<MediaMetadata>> newMusicListByGenre = new ConcurrentHashMap<>();
 
+        for (MutableMediaMetadata m : mMusicListById.values()) {
+            String genre = m.metadata.getString(MediaMetadata.METADATA_KEY_GENRE);
+            List<MediaMetadata> list = newMusicListByGenre.get(genre);
+            if (list == null) {
+                list = new ArrayList<>();
+                newMusicListByGenre.put(genre, list);
+            }
+            list.add(m.metadata);
+        }
+        mMusicListByGenre = newMusicListByGenre;
+    }
+
+    private synchronized void retrieveMedia() {
         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);
-
+                JSONObject jsonObj = fetchJSONFromUrl(CATALOG_URL);
+                if (jsonObj == null) {
+                    return;
+                }
                 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);
+                        String musicId = item.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
+                        mMusicListById.put(musicId, new MutableMediaMetadata(musicId, item));
                     }
+                    buildListsByGenre();
                 }
                 mCurrentState = State.INITIALIZED;
             }
-        } catch (RuntimeException | JSONException e) {
+        } catch (JSONException e) {
             LogHelper.e(TAG, e, "Could not retrieve music list");
         } finally {
             if (mCurrentState != State.INITIALIZED) {
@@ -210,10 +244,6 @@
                 // retries (eg if the network connection is temporary unavailable)
                 mCurrentState = State.NON_INITIALIZED;
             }
-            initializationLock.unlock();
-            if (callback != null) {
-                callback.onMusicCatalogReady(mCurrentState == State.INITIALIZED);
-            }
         }
     }
 
@@ -263,19 +293,18 @@
      * Download a JSON file from a server, parse the content and return the JSON
      * object.
      *
-     * @param urlString
-     * @return
+     * @return result JSONObject containing the parsed representation.
      */
-    private JSONObject parseUrl(String urlString) {
+    private JSONObject fetchJSONFromUrl(String urlString) {
         InputStream is = null;
         try {
-            java.net.URL url = new java.net.URL(urlString);
+            URL url = new URL(urlString);
             URLConnection urlConnection = url.openConnection();
             is = new BufferedInputStream(urlConnection.getInputStream());
             BufferedReader reader = new BufferedReader(new InputStreamReader(
                     urlConnection.getInputStream(), "iso-8859-1"));
             StringBuilder sb = new StringBuilder();
-            String line = null;
+            String line;
             while ((line = reader.readLine()) != null) {
                 sb.append(line);
             }
@@ -293,4 +322,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/model/MutableMediaMetadata.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/model/MutableMediaMetadata.java
new file mode 100644
index 0000000..1ee9d61
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/model/MutableMediaMetadata.java
@@ -0,0 +1,54 @@
+/*
+ * 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.mediabrowserservice.model;
+
+import android.media.MediaMetadata;
+import android.text.TextUtils;
+
+/**
+ * Holder class that encapsulates a MediaMetadata and allows the actual metadata to be modified
+ * without requiring to rebuild the collections the metadata is in.
+ */
+public class MutableMediaMetadata {
+
+    public MediaMetadata metadata;
+    public final String trackId;
+
+    public MutableMediaMetadata(String trackId, MediaMetadata metadata) {
+        this.metadata = metadata;
+        this.trackId = trackId;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || o.getClass() != MutableMediaMetadata.class) {
+            return false;
+        }
+
+        MutableMediaMetadata that = (MutableMediaMetadata) o;
+
+        return TextUtils.equals(trackId, that.trackId);
+    }
+
+    @Override
+    public int hashCode() {
+        return trackId.hashCode();
+    }
+}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/BitmapHelper.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/BitmapHelper.java
index 5f0e767..7325130 100644
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/BitmapHelper.java
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/BitmapHelper.java
@@ -18,22 +18,26 @@
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 
+import java.io.BufferedInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.HttpURLConnection;
 import java.net.URL;
 
 public class BitmapHelper {
+    private static final String TAG = LogHelper.makeLogTag(BitmapHelper.class);
 
-    // 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;
+    // Max read limit that we allow our input stream to mark/reset.
+    private static final int MAX_READ_LIMIT_PER_IMG = 1024 * 1024;
 
-    // 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 Bitmap scaleBitmap(Bitmap src, int maxWidth, int maxHeight) {
+       double scaleFactor = Math.min(
+           ((double) maxWidth)/src.getWidth(), ((double) maxHeight)/src.getHeight());
+        return Bitmap.createScaledBitmap(src,
+            (int) (src.getWidth() * scaleFactor), (int) (src.getHeight() * scaleFactor), false);
+    }
 
-    public static final Bitmap scaleBitmap(int scaleFactor, InputStream is) {
+    public static Bitmap scaleBitmap(int scaleFactor, InputStream is) {
         // Get the dimensions of the bitmap
         BitmapFactory.Options bmOptions = new BitmapFactory.Options();
 
@@ -41,11 +45,10 @@
         bmOptions.inJustDecodeBounds = false;
         bmOptions.inSampleSize = scaleFactor;
 
-        Bitmap bitmap = BitmapFactory.decodeStream(is, null, bmOptions);
-        return bitmap;
+        return BitmapFactory.decodeStream(is, null, bmOptions);
     }
 
-    public static final int findScaleFactor(int targetW, int targetH, InputStream is) {
+    public static int findScaleFactor(int targetW, int targetH, InputStream is) {
         // Get the dimensions of the bitmap
         BitmapFactory.Options bmOptions = new BitmapFactory.Options();
         bmOptions.inJustDecodeBounds = true;
@@ -57,21 +60,24 @@
         return Math.min(actualW/targetW, actualH/targetH);
     }
 
-    public static final Bitmap fetchAndRescaleBitmap(String uri, int width, int height)
+    @SuppressWarnings("SameParameterValue")
+    public static 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;
+        BufferedInputStream is = null;
+        try {
+            HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
+            is = new BufferedInputStream(urlConnection.getInputStream());
+            is.mark(MAX_READ_LIMIT_PER_IMG);
+            int scaleFactor = findScaleFactor(width, height, is);
+            LogHelper.d(TAG, "Scaling bitmap ", uri, " by factor ", scaleFactor, " to support ",
+                    width, "x", height, "requested dimension");
+            is.reset();
+            return scaleBitmap(scaleFactor, is);
+        } finally {
+            if (is != null) {
+                is.close();
+            }
+        }
     }
-
 }
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/CarHelper.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/CarHelper.java
new file mode 100644
index 0000000..74861ba
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/CarHelper.java
@@ -0,0 +1,55 @@
+/*
+ * 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.mediabrowserservice.utils;
+
+import android.os.Bundle;
+
+public class CarHelper {
+    private static final String AUTO_APP_PACKAGE_NAME = "com.google.android.projection.gearhead";
+
+    // Use these extras to reserve space for the corresponding actions, even when they are disabled
+    // in the playbackstate, so the custom actions don't reflow.
+    private static final String SLOT_RESERVATION_SKIP_TO_NEXT =
+            "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT";
+    private static final String SLOT_RESERVATION_SKIP_TO_PREV =
+            "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS";
+    private static final String SLOT_RESERVATION_QUEUE =
+            "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE";
+
+
+    public static boolean isValidCarPackage(String packageName) {
+        return AUTO_APP_PACKAGE_NAME.equals(packageName);
+    }
+
+    public static void setSlotReservationFlags(Bundle extras, boolean reservePlayingQueueSlot,
+          boolean reserveSkipToNextSlot, boolean reserveSkipToPrevSlot) {
+        if (reservePlayingQueueSlot) {
+            extras.putBoolean(SLOT_RESERVATION_QUEUE, true);
+        } else {
+            extras.remove(SLOT_RESERVATION_QUEUE);
+        }
+        if (reserveSkipToPrevSlot) {
+            extras.putBoolean(SLOT_RESERVATION_SKIP_TO_PREV, true);
+        } else {
+            extras.remove(SLOT_RESERVATION_SKIP_TO_PREV);
+        }
+        if (reserveSkipToNextSlot) {
+            extras.putBoolean(SLOT_RESERVATION_SKIP_TO_NEXT, true);
+        } else {
+            extras.remove(SLOT_RESERVATION_SKIP_TO_NEXT);
+        }
+    }
+}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/LogHelper.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/LogHelper.java
index bf5e3a7..09d14d2 100644
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/LogHelper.java
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/LogHelper.java
@@ -20,6 +20,7 @@
 import com.example.android.mediabrowserservice.BuildConfig;
 
 public class LogHelper {
+
     private static final String LOG_PREFIX = "sample_";
     private static final int LOG_PREFIX_LENGTH = LOG_PREFIX.length();
     private static final int MAX_LOG_TAG_LENGTH = 23;
@@ -32,6 +33,14 @@
         return LOG_PREFIX + str;
     }
 
+    /**
+     * Don't use this when obfuscating class names!
+     */
+    public static String makeLogTag(Class cls) {
+        return makeLogTag(cls.getSimpleName());
+    }
+
+
     public static void v(String tag, Object... messages) {
         // Only log VERBOSE if build type is DEBUG
         if (BuildConfig.DEBUG) {
@@ -67,13 +76,14 @@
     }
 
     public static void log(String tag, int level, Throwable t, Object... messages) {
-        if (messages != null && Log.isLoggable(tag, level)) {
+        if (Log.isLoggable(tag, level)) {
             String message;
-            if (messages.length == 1) {
-                message = messages[0] == null ? null : messages[0].toString();
+            if (t == null && messages != null && messages.length == 1) {
+                // handle this common case without the extra cost of creating a stringbuffer:
+                message = messages[0].toString();
             } else {
                 StringBuilder sb = new StringBuilder();
-                for (Object m: messages) {
+                if (messages != null) for (Object m : messages) {
                     sb.append(m);
                 }
                 if (t != null) {
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/MediaIDHelper.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/MediaIDHelper.java
index ca9dfba..604cf8a 100644
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/MediaIDHelper.java
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/MediaIDHelper.java
@@ -16,32 +16,44 @@
 
 package com.example.android.mediabrowserservice.utils;
 
-import android.media.MediaMetadata;
+import java.util.Arrays;
 
 /**
  * Utility class to help on queue related tasks.
  */
 public class MediaIDHelper {
 
-    private static final String TAG = LogHelper.makeLogTag(MediaIDHelper.class.getSimpleName());
+    private static final String TAG = LogHelper.makeLogTag(MediaIDHelper.class);
 
     // 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 MEDIA_ID_MUSICS_BY_SEARCH = "__BY_SEARCH__";
 
-    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
+    private static final char CATEGORY_SEPARATOR = '/';
+    private static final char LEAF_SEPARATOR = '|';
+
+    public static String createMediaID(String musicID, String... categories) {
+        // 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);
+        StringBuilder sb = new StringBuilder();
+        if (categories != null && categories.length > 0) {
+            sb.append(categories[0]);
+            for (int i=1; i < categories.length; i++) {
+                sb.append(CATEGORY_SEPARATOR).append(categories[i]);
+            }
+        }
+        if (musicID != null) {
+            sb.append(LEAF_SEPARATOR).append(musicID);
+        }
+        return sb.toString();
     }
 
-    public static final String createBrowseCategoryMediaID(String categoryType, String categoryValue) {
-        return categoryType + "/" + categoryValue;
+    public static String createBrowseCategoryMediaID(String categoryType, String categoryValue) {
+        return categoryType + CATEGORY_SEPARATOR + categoryValue;
     }
 
     /**
@@ -50,12 +62,15 @@
      * 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
+     * @param mediaID that contains the musicID
+     * @return musicID
      */
-    public static final String extractMusicIDFromMediaID(String musicID) {
-        String[] segments = musicID.split("\\|", 2);
-        return segments.length == 2 ? segments[1] : null;
+    public static String extractMusicIDFromMediaID(String mediaID) {
+        int pos = mediaID.indexOf(LEAF_SEPARATOR);
+        if (pos >= 0) {
+            return mediaID.substring(pos+1);
+        }
+        return null;
     }
 
     /**
@@ -64,25 +79,37 @@
      * 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
+     * @param mediaID that contains a category and categoryValue.
      */
-    public static final String[] extractBrowseCategoryFromMediaID(String mediaID) {
-        if (mediaID.indexOf('|') >= 0) {
-            mediaID = mediaID.split("\\|")[0];
+    public static String[] getHierarchy(String mediaID) {
+        int pos = mediaID.indexOf(LEAF_SEPARATOR);
+        if (pos >= 0) {
+            mediaID = mediaID.substring(0, pos);
         }
-        if (mediaID.indexOf('/') == 0) {
-            return new String[]{mediaID, null};
-        } else {
-            return mediaID.split("/", 2);
-        }
+        return mediaID.split(String.valueOf(CATEGORY_SEPARATOR));
     }
 
-    public static final String extractBrowseCategoryValueFromMediaID(String mediaID) {
-        String[] categoryAndValue = extractBrowseCategoryFromMediaID(mediaID);
-        if (categoryAndValue != null && categoryAndValue.length == 2) {
-            return categoryAndValue[1];
+    public static String extractBrowseCategoryValueFromMediaID(String mediaID) {
+        String[] hierarchy = getHierarchy(mediaID);
+        if (hierarchy != null && hierarchy.length == 2) {
+            return hierarchy[1];
         }
         return null;
     }
-}
\ No newline at end of file
+
+    private static boolean isBrowseable(String mediaID) {
+        return mediaID.indexOf(LEAF_SEPARATOR) < 0;
+    }
+
+    public static String getParentMediaID(String mediaID) {
+        String[] hierarchy = getHierarchy(mediaID);
+        if (!isBrowseable(mediaID)) {
+            return createMediaID(null, hierarchy);
+        }
+        if (hierarchy == null || hierarchy.length <= 1) {
+            return MEDIA_ID_ROOT;
+        }
+        String[] parentHierarchy = Arrays.copyOf(hierarchy, hierarchy.length-1);
+        return createMediaID(null, parentHierarchy);
+    }
+}
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/QueueHelper.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/QueueHelper.java
index ba273af..9a2caa8 100644
--- a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/QueueHelper.java
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/QueueHelper.java
@@ -22,52 +22,64 @@
 import com.example.android.mediabrowserservice.model.MusicProvider;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 
 import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_MUSICS_BY_GENRE;
+import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_MUSICS_BY_SEARCH;
 
 /**
  * Utility class to help on queue related tasks.
  */
 public class QueueHelper {
 
-    private static final String TAG = LogHelper.makeLogTag(QueueHelper.class.getSimpleName());
+    private static final String TAG = LogHelper.makeLogTag(QueueHelper.class);
 
-    public static final List<MediaSession.QueueItem> getPlayingQueue(String mediaId,
+    public static List<MediaSession.QueueItem> getPlayingQueue(String mediaId,
             MusicProvider musicProvider) {
 
-        // extract the category and unique music ID from the media ID:
-        String[] category = MediaIDHelper.extractBrowseCategoryFromMediaID(mediaId);
+        // extract the browsing hierarchy from the media ID:
+        String[] hierarchy = MediaIDHelper.getHierarchy(mediaId);
 
-        // This sample only supports genre category.
-        if (!category[0].equals(MEDIA_ID_MUSICS_BY_GENRE) || category.length != 2) {
+        if (hierarchy.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);
+        String categoryType = hierarchy[0];
+        String categoryValue = hierarchy[1];
+        LogHelper.d(TAG, "Creating playing queue for ", categoryType, ",  ", categoryValue);
 
-        List<MediaSession.QueueItem> queue = convertToQueue(
-                musicProvider.getMusicsByGenre(categoryValue));
+        Iterable<MediaMetadata> tracks = null;
+        // This sample only supports genre and by_search category types.
+        if (categoryType.equals(MEDIA_ID_MUSICS_BY_GENRE)) {
+            tracks = musicProvider.getMusicsByGenre(categoryValue);
+        } else if (categoryType.equals(MEDIA_ID_MUSICS_BY_SEARCH)) {
+            tracks = musicProvider.searchMusic(categoryValue);
+        }
 
-        return queue;
+        if (tracks == null) {
+            LogHelper.e(TAG, "Unrecognized category type: ", categoryType, " for mediaId ", mediaId);
+            return null;
+        }
+
+        return convertToQueue(tracks, hierarchy[0], hierarchy[1]);
     }
 
-    public static final List<MediaSession.QueueItem> getPlayingQueueFromSearch(String query,
+    public static List<MediaSession.QueueItem> getPlayingQueueFromSearch(String query,
             MusicProvider musicProvider) {
 
-        LogHelper.e(TAG, "Creating playing queue for musics from search ", query);
+        LogHelper.d(TAG, "Creating playing queue for musics from search ", query);
 
-        return convertToQueue(musicProvider.searchMusics(query));
+        return convertToQueue(musicProvider.searchMusic(query), MEDIA_ID_MUSICS_BY_SEARCH, query);
     }
 
 
-    public static final int getMusicIndexOnQueue(Iterable<MediaSession.QueueItem> queue,
+    public static int getMusicIndexOnQueue(Iterable<MediaSession.QueueItem> queue,
              String mediaId) {
         int index = 0;
-        for (MediaSession.QueueItem item: queue) {
+        for (MediaSession.QueueItem item : queue) {
             if (mediaId.equals(item.getDescription().getMediaId())) {
                 return index;
             }
@@ -76,10 +88,10 @@
         return -1;
     }
 
-    public static final int getMusicIndexOnQueue(Iterable<MediaSession.QueueItem> queue,
+    public static int getMusicIndexOnQueue(Iterable<MediaSession.QueueItem> queue,
              long queueId) {
         int index = 0;
-        for (MediaSession.QueueItem item: queue) {
+        for (MediaSession.QueueItem item : queue) {
             if (queueId == item.getQueueId()) {
                 return index;
             }
@@ -88,15 +100,25 @@
         return -1;
     }
 
-    private static final List<MediaSession.QueueItem> convertToQueue(
-            Iterable<MediaMetadata> tracks) {
+    private static List<MediaSession.QueueItem> convertToQueue(
+            Iterable<MediaMetadata> tracks, String... categories) {
         List<MediaSession.QueueItem> queue = new ArrayList<>();
         int count = 0;
         for (MediaMetadata track : tracks) {
+
+            // We create a hierarchy-aware mediaID, so we know what the queue is about by looking
+            // at the QueueItem media IDs.
+            String hierarchyAwareMediaID = MediaIDHelper.createMediaID(
+                    track.getDescription().getMediaId(), categories);
+
+            MediaMetadata trackCopy = new MediaMetadata.Builder(track)
+                    .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, hierarchyAwareMediaID)
+                    .build();
+
             // 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++);
+                    trackCopy.getDescription(), count++);
             queue.add(item);
         }
         return queue;
@@ -105,25 +127,23 @@
 
     /**
      * Create a random queue. For simplicity sake, instead of a random queue, we create a
-     * queue using the first genre,
+     * queue using the first genre.
      *
-     * @param musicProvider
-     * @return
+     * @param musicProvider the provider used for fetching music.
+     * @return list containing {@link android.media.session.MediaSession.QueueItem}'s
      */
-    public static final List<MediaSession.QueueItem> getRandomQueue(MusicProvider musicProvider) {
+    public static List<MediaSession.QueueItem> getRandomQueue(MusicProvider musicProvider) {
         Iterator<String> genres = musicProvider.getGenres().iterator();
         if (!genres.hasNext()) {
-            return new ArrayList<>();
+            return Collections.emptyList();
         }
         String genre = genres.next();
         Iterable<MediaMetadata> tracks = musicProvider.getMusicsByGenre(genre);
 
-        return convertToQueue(tracks);
+        return convertToQueue(tracks, MEDIA_ID_MUSICS_BY_GENRE, genre);
     }
 
-
-
-    public static final boolean isIndexPlayable(int index, List<MediaSession.QueueItem> queue) {
+    public static boolean isIndexPlayable(int index, List<MediaSession.QueueItem> queue) {
         return (queue != null && index >= 0 && index < queue.size());
     }
 }
diff --git a/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/ResourceHelper.java b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/ResourceHelper.java
new file mode 100644
index 0000000..ed4dcd0
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/java/com/example/android/mediabrowserservice/utils/ResourceHelper.java
@@ -0,0 +1,53 @@
+/*
+* 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.mediabrowserservice.utils;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+
+/**
+ * Generic reusable methods to handle resources.
+ */
+public class ResourceHelper {
+    /**
+     * Get a color value from a theme attribute.
+     * @param context used for getting the color.
+     * @param attribute theme attribute.
+     * @param defaultColor default to use.
+     * @return color value
+     */
+    public static int getThemeColor(Context context, int attribute, int defaultColor) {
+        int themeColor = 0;
+        String packageName = context.getPackageName();
+        try {
+            Context packageContext = context.createPackageContext(packageName, 0);
+            ApplicationInfo applicationInfo =
+                context.getPackageManager().getApplicationInfo(packageName, 0);
+            packageContext.setTheme(applicationInfo.theme);
+            Resources.Theme theme = packageContext.getTheme();
+            TypedArray ta = theme.obtainStyledAttributes(new int[] {attribute});
+            themeColor = ta.getColor(0, defaultColor);
+            ta.recycle();
+        } catch (PackageManager.NameNotFoundException e) {
+            e.printStackTrace();
+        }
+        return themeColor;
+    }
+}
diff --git a/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_notification.png b/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_notification.png
index d8ea5a9..a8cba40 100644
--- a/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_notification.png
+++ b/media/MediaBrowserService/Application/src/main/res/drawable-hdpi/ic_notification.png
Binary files differ
diff --git a/media/MediaBrowserService/Application/src/main/res/values-v21/styles.xml b/media/MediaBrowserService/Application/src/main/res/values-v21/styles.xml
deleted file mode 100644
index 21bb211..0000000
--- a/media/MediaBrowserService/Application/src/main/res/values-v21/styles.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-<?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="android:Theme.Material">
-        <!-- colorPrimary is used for Notification icon and bottom facet bar icons
-        and overflow actions -->
-        <item name="android:colorPrimary">#ffff5722</item>
-
-        <!-- colorPrimaryDark is used for background -->
-        <item name="android:colorPrimaryDark">#ffbf360c</item>
-
-        <!-- colorAccent is sparingly used for accents, like floating action button highlight,
-        progress on playbar-->
-        <item name="android:colorAccent">#ffff5722</item>
-
-    </style>
-
-</resources>
diff --git a/media/MediaBrowserService/Application/src/main/res/values/styles.xml b/media/MediaBrowserService/Application/src/main/res/values/styles.xml
index 3be59c1..35a3e7a 100644
--- a/media/MediaBrowserService/Application/src/main/res/values/styles.xml
+++ b/media/MediaBrowserService/Application/src/main/res/values/styles.xml
@@ -15,12 +15,27 @@
   limitations under the License.
 -->
 <resources>
-
-
-    <style name="AppTheme" parent="AppBaseTheme">
+    <style name="AppTheme" parent="android:Theme.Material">
+        <item name="android:colorPrimary">#ffff5722</item>
+        <item name="android:colorPrimaryDark">#ffbf360c</item>
+        <item name="android:colorAccent">#ffff5722</item>
     </style>
 
-    <style name="AppBaseTheme" parent="android:Theme.Light">
+    <style name="CarTheme" parent="AppTheme">
+        <!-- colorPrimaryDark is currently used in Android Auto for:
+             - App background
+             - Drawer right side ("more" custom actions) background
+             - Notification icon badge tinting
+             - Overview “now playing” icon tinting
+         -->
+        <item name="android:colorPrimaryDark">#ffbf360c</item>
+
+        <!-- colorAccent is used in Android Auto for:
+             - Spinner
+             - progress bar
+             - floating action button background (Play/Pause in media apps)
+         -->
+        <item name="android:colorAccent">#ffff5722</item>
     </style>
 
 </resources>
\ No newline at end of file
diff --git a/media/MediaBrowserService/Application/src/main/res/xml/allowed_media_browser_callers.xml b/media/MediaBrowserService/Application/src/main/res/xml/allowed_media_browser_callers.xml
new file mode 100644
index 0000000..5c326cf
--- /dev/null
+++ b/media/MediaBrowserService/Application/src/main/res/xml/allowed_media_browser_callers.xml
@@ -0,0 +1,174 @@
+<?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.
+-->
+<allowed_callers>
+    <signing_certificate name="Android Auto" release="false"
+                         package="com.google.android.projection.gearhead">
+        MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYD
+        VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g
+        VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE
+        AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe
+        Fw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzET
+        MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G
+        A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p
+        ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI
+        hvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR
+        24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVy
+        xW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8X
+        W8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC
+        69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexA
+        cKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkw
+        HQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0c
+        xb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE
+        CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH
+        QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG
+        CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1Ud
+        EwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrP
+        zgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXcla
+        XjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05a
+        IskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+a
+        ayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUW
+        Ev9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
+    </signing_certificate>
+    <signing_certificate name="Android Auto" release="false"
+                         package="com.google.android.projection.gearhead">
+        MIIDvTCCAqWgAwIBAgIJAOfkBvDXw5bzMA0GCSqGSIb3DQEBBQUAMHUxCzAJBgNV
+        BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW
+        aWV3MRQwEgYDVQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDERMA8G
+        A1UEAwwIZ2VhcmhlYWQwHhcNMTQwNTI3MjMwMjUxWhcNNDExMDEyMjMwMjUxWjB1
+        MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91
+        bnRhaW4gVmlldzEUMBIGA1UECgwLR29vZ2xlIEluYy4xEDAOBgNVBAsMB0FuZHJv
+        aWQxETAPBgNVBAMMCGdlYXJoZWFkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+        CgKCAQEAou7wwBKFyznqpRretJ3EVp55/Yr049Ag5wlGvrCnjIP8DrMrU+skfKe1
+        DmwpsLNtnhhiNH+J000Lok3hc8jdWKeKOopzKGDNvL/HvnS70Zyk26gj9jtMMHz9
+        2aZdpmwD67FNmTlG2FERr+TwMD5agaPnsFR2zla6ugUvHGzz65YDxpCZsQ/TowyD
+        LnxgMagvhvS+Oex3yh2FN7pJfwS03KdGdkWPbLqf9Fem09s5jjeZW/O3RgnKoRPI
+        J4QLK70efjAZqJyBGcDZyQMwOs+8HIknraf8+cRZJDzqOx7rttl8M3KGB2EFljTp
+        6/FyxJLnAo6QlXn7GrYalTI0yLU9dQIDAQABo1AwTjAdBgNVHQ4EFgQU9QPJ5xJE
+        DA8MDQMrj0hm2/A2BRkwHwYDVR0jBBgwFoAU9QPJ5xJEDA8MDQMrj0hm2/A2BRkw
+        DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEADcr5h1FR8IpmN4hSsUA9
+        SnCQVyXa1GQhzpQgRbF+npkgOn2Mebp8bd28VpfgooD2OBNQXCUcZkn7pWj++ut9
+        HhObHVaV5FNg0pdDqLna9QZ9Y4oS+ZrijK70XZ/EjlYUHvhu0pIjZAbD8CmCFlow
+        SR55qCSjM5iS37LZB32SMr1BBiYrNAvncKjYQVK8ctTRzhpNQQPBgXBA98Xl+d1D
+        Py00JWQuF0ssmhKcJuvfdEnFF7Hvaxz/gCQ9nzarQI3CJB8dOXVwF8mcyDRBz4JR
+        +YDpXo6BD+fGt15ov+zmqC8xaT9P1/JgoDXiMhy/6rwgdi9WxPf8mb7TnBC+CksX
+        0A==
+    </signing_certificate>
+    <signing_certificate name="Android Auto" release="true"
+                         package="com.google.android.projection.gearhead">
+        MIIDvTCCAqWgAwIBAgIJAMePnkuTQTAGMA0GCSqGSIb3DQEBBQUAMHUxCzAJBgNV
+        BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW
+        aWV3MRQwEgYDVQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDERMA8G
+        A1UEAwwIZ2VhcmhlYWQwHhcNMTQwNTI3MjMwNTM0WhcNNDExMDEyMjMwNTM0WjB1
+        MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91
+        bnRhaW4gVmlldzEUMBIGA1UECgwLR29vZ2xlIEluYy4xEDAOBgNVBAsMB0FuZHJv
+        aWQxETAPBgNVBAMMCGdlYXJoZWFkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+        CgKCAQEA050XDkNIsVRMX2wTvVplpCu4OtnyNK2v5B7PS+DggmH2yuZiwpTurdKD
+        Q9R9UzxH9U4lsC+mIxXkiBYKIWNVgMtiTgxkEy7cgWvdYHgNYpFu8IxZKYDyXes+
+        02pfvpu63MIBD/PnvVFipo1oUrbfetj+mroEpjnA71gUS0Ok+H6XWWsmb8xFHQVM
+        oZWEIzsUJ2nhm8EcnPkAPfNZAG++XLPROoRQCaswyYsd42JuYAP3CwZuhDcUbMWm
+        k7rBi9BVQ8gmkrbwqo94A7qStLUp3NyCmlKSWHaZ05SspEPwsfctka0oXG5bhgT6
+        67EMCzQ+YsFN1oJRL7Qq+mMQjFJs3wIDAQABo1AwTjAdBgNVHQ4EFgQUGvBfYNeu
+        6JSJUnJZCiaBGsnXztswHwYDVR0jBBgwFoAUGvBfYNeu6JSJUnJZCiaBGsnXztsw
+        DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAlGsDY0EPu3NBSH5k6iw/
+        wJh9e3xMwS17ErKGlhyWogxJMzLjAN6g0aCPHxB40IQC+8qAl+RL7VQx6oxttf0m
+        31yUGQPcNYbt2CxBTCAr885oLK5t2TAi5tQzhd6ZEYihWSUWUd/X8BQRouxboss9
+        QbBA/iIx0OpDaxiAcq7Cb67TheXZDxGuQ8fmHYbLx84pEvm3DQOB/LIMkkpQSfEC
+        1f+oP1zB3urPU/dSvED/LCgOdrpxZ5di7SwSyue+Vq/TZQy34tPygEzD2d8hFlh/
+        yfhWkMizOeIXcayVAQdNn5zpBkuay1skGOjQQ5kTbDcDzigO2R2rqn6HCd9l5Z0W
+        IQ==
+    </signing_certificate>
+    <signing_certificate name="Media Browser Service Simulator" release="true"
+                         package="com.google.android.mediasimulator">
+        MIIDvTCCAqWgAwIBAgIJAMePnkuTQTAGMA0GCSqGSIb3DQEBBQUAMHUxCzAJBgNV
+        BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW
+        aWV3MRQwEgYDVQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDERMA8G
+        A1UEAwwIZ2VhcmhlYWQwHhcNMTQwNTI3MjMwNTM0WhcNNDExMDEyMjMwNTM0WjB1
+        MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91
+        bnRhaW4gVmlldzEUMBIGA1UECgwLR29vZ2xlIEluYy4xEDAOBgNVBAsMB0FuZHJv
+        aWQxETAPBgNVBAMMCGdlYXJoZWFkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+        CgKCAQEA050XDkNIsVRMX2wTvVplpCu4OtnyNK2v5B7PS+DggmH2yuZiwpTurdKD
+        Q9R9UzxH9U4lsC+mIxXkiBYKIWNVgMtiTgxkEy7cgWvdYHgNYpFu8IxZKYDyXes+
+        02pfvpu63MIBD/PnvVFipo1oUrbfetj+mroEpjnA71gUS0Ok+H6XWWsmb8xFHQVM
+        oZWEIzsUJ2nhm8EcnPkAPfNZAG++XLPROoRQCaswyYsd42JuYAP3CwZuhDcUbMWm
+        k7rBi9BVQ8gmkrbwqo94A7qStLUp3NyCmlKSWHaZ05SspEPwsfctka0oXG5bhgT6
+        67EMCzQ+YsFN1oJRL7Qq+mMQjFJs3wIDAQABo1AwTjAdBgNVHQ4EFgQUGvBfYNeu
+        6JSJUnJZCiaBGsnXztswHwYDVR0jBBgwFoAUGvBfYNeu6JSJUnJZCiaBGsnXztsw
+        DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAlGsDY0EPu3NBSH5k6iw/
+        wJh9e3xMwS17ErKGlhyWogxJMzLjAN6g0aCPHxB40IQC+8qAl+RL7VQx6oxttf0m
+        31yUGQPcNYbt2CxBTCAr885oLK5t2TAi5tQzhd6ZEYihWSUWUd/X8BQRouxboss9
+        QbBA/iIx0OpDaxiAcq7Cb67TheXZDxGuQ8fmHYbLx84pEvm3DQOB/LIMkkpQSfEC
+        1f+oP1zB3urPU/dSvED/LCgOdrpxZ5di7SwSyue+Vq/TZQy34tPygEzD2d8hFlh/
+        yfhWkMizOeIXcayVAQdNn5zpBkuay1skGOjQQ5kTbDcDzigO2R2rqn6HCd9l5Z0W
+        IQ==
+    </signing_certificate>
+    <signing_certificate name="Android Auto Simulator" release="true"
+                         package="com.google.android.autosimulator">
+        MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYD
+        VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g
+        VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE
+        AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe
+        Fw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzET
+        MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G
+        A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p
+        ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI
+        hvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR
+        24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVy
+        xW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8X
+        W8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC
+        69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexA
+        cKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkw
+        HQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0c
+        xb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE
+        CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH
+        QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG
+        CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1Ud
+        EwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrP
+        zgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXcla
+        XjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05a
+        IskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+a
+        ayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUW
+        Ev9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
+    </signing_certificate>
+    <signing_certificate name="Media Browser Simulator" release="true"
+                         package="com.google.android.mediasimulator">
+        MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYD
+        VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g
+        VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE
+        AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe
+        Fw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzET
+        MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G
+        A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p
+        ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI
+        hvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR
+        24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVy
+        xW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8X
+        W8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC
+        69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexA
+        cKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkw
+        HQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0c
+        xb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE
+        CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH
+        QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG
+        CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1Ud
+        EwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrP
+        zgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXcla
+        XjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05a
+        IskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+a
+        ayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUW
+        Ev9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
+    </signing_certificate>
+</allowed_callers>