Merge "TIF: Revisit types in TvInputInfo and TvContract.Channels." into lmp-dev
diff --git a/Android.mk b/Android.mk
index 79d13d9..7dfa6a0 100644
--- a/Android.mk
+++ b/Android.mk
@@ -322,15 +322,17 @@
 	media/java/android/media/IRemoteVolumeObserver.aidl \
 	media/java/android/media/IRingtonePlayer.aidl \
 	media/java/android/media/IVolumeController.aidl \
+	media/java/android/media/browse/IMediaBrowserService.aidl \
+	media/java/android/media/browse/IMediaBrowserServiceCallbacks.aidl \
+	media/java/android/media/projection/IMediaProjection.aidl \
+	media/java/android/media/projection/IMediaProjectionCallback.aidl \
+	media/java/android/media/projection/IMediaProjectionManager.aidl \
 	media/java/android/media/routing/IMediaRouteService.aidl \
 	media/java/android/media/routing/IMediaRouteClientCallback.aidl \
 	media/java/android/media/routing/IMediaRouter.aidl \
 	media/java/android/media/routing/IMediaRouterDelegate.aidl \
 	media/java/android/media/routing/IMediaRouterRoutingCallback.aidl \
 	media/java/android/media/routing/IMediaRouterStateCallback.aidl \
-	media/java/android/media/projection/IMediaProjection.aidl \
-	media/java/android/media/projection/IMediaProjectionCallback.aidl \
-	media/java/android/media/projection/IMediaProjectionManager.aidl \
 	media/java/android/media/session/IActiveSessionsListener.aidl \
 	media/java/android/media/session/ISessionController.aidl \
 	media/java/android/media/session/ISessionControllerCallback.aidl \
diff --git a/api/current.txt b/api/current.txt
index 2821114..ce5e366 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -3820,6 +3820,7 @@
     method public void startWatchingMode(java.lang.String, java.lang.String, android.app.AppOpsManager.OnOpChangedListener);
     method public void stopWatchingMode(android.app.AppOpsManager.OnOpChangedListener);
     field public static final int MODE_ALLOWED = 0; // 0x0
+    field public static final int MODE_DEFAULT = 3; // 0x3
     field public static final int MODE_ERRORED = 2; // 0x2
     field public static final int MODE_IGNORED = 1; // 0x1
     field public static final java.lang.String OPSTR_COARSE_LOCATION = "android:coarse_location";
@@ -8877,6 +8878,7 @@
     field public static final android.os.Parcelable.Creator CREATOR;
     field public static final int FLAG_COSTS_MONEY = 1; // 0x1
     field public static final int PROTECTION_DANGEROUS = 1; // 0x1
+    field public static final int PROTECTION_FLAG_APPOP = 64; // 0x40
     field public static final int PROTECTION_FLAG_DEVELOPMENT = 32; // 0x20
     field public static final int PROTECTION_FLAG_SYSTEM = 16; // 0x10
     field public static final int PROTECTION_MASK_BASE = 15; // 0xf
@@ -16154,6 +16156,77 @@
 
 }
 
+package android.media.browse {
+
+  public final class MediaBrowser {
+    ctor public MediaBrowser(android.content.Context, android.content.ComponentName, android.media.browse.MediaBrowser.ConnectionCallback, android.os.Bundle);
+    method public void connect();
+    method public void disconnect();
+    method public android.net.Uri getRoot();
+    method public android.media.session.MediaSession.Token getSessionToken();
+    method public boolean isConnected();
+    method public void loadThumbnail(android.net.Uri, int, int, int, android.media.browse.MediaBrowser.ThumbnailCallback);
+    method public void subscribe(android.net.Uri, android.media.browse.MediaBrowser.SubscriptionCallback);
+    method public void unsubscribe(android.net.Uri);
+  }
+
+  public static class MediaBrowser.ConnectionCallback {
+    ctor public MediaBrowser.ConnectionCallback();
+    method public void onConnected();
+    method public void onConnectionFailed();
+    method public void onConnectionSuspended();
+  }
+
+  public static abstract class MediaBrowser.SubscriptionCallback {
+    ctor public MediaBrowser.SubscriptionCallback();
+    method public void onChildrenLoaded(android.net.Uri, java.util.List<android.media.browse.MediaBrowserItem>);
+    method public void onError(android.net.Uri);
+  }
+
+  public static abstract class MediaBrowser.ThumbnailCallback {
+    ctor public MediaBrowser.ThumbnailCallback();
+    method public void onError(android.net.Uri);
+    method public void onThumbnailLoaded(android.net.Uri, android.graphics.Bitmap);
+  }
+
+  public final class MediaBrowserItem implements android.os.Parcelable {
+    method public int describeContents();
+    method public android.os.Bundle getExtras();
+    method public int getFlags();
+    method public java.lang.CharSequence getSummary();
+    method public java.lang.CharSequence getTitle();
+    method public android.net.Uri getUri();
+    method public boolean isBrowsable();
+    method public boolean isPlayable();
+    method public void writeToParcel(android.os.Parcel, int);
+    field public static final android.os.Parcelable.Creator CREATOR;
+    field public static final int FLAG_BROWSABLE = 1; // 0x1
+    field public static final int FLAG_PLAYABLE = 2; // 0x2
+  }
+
+  public static final class MediaBrowserItem.Builder {
+    ctor public MediaBrowserItem.Builder(android.net.Uri, int, java.lang.CharSequence);
+    method public android.media.browse.MediaBrowserItem build();
+    method public android.media.browse.MediaBrowserItem.Builder setExtras(android.os.Bundle);
+    method public android.media.browse.MediaBrowserItem.Builder setSummary(java.lang.CharSequence);
+  }
+
+  public abstract class MediaBrowserService extends android.app.Service {
+    ctor public MediaBrowserService();
+    method public void dump(java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[]);
+    method public android.media.session.MediaSession.Token getSessionToken();
+    method public void notifyChange();
+    method public void notifyChildrenChanged(android.net.Uri);
+    method public android.os.IBinder onBind(android.content.Intent);
+    method public abstract android.net.Uri onGetRoot(java.lang.String, int, android.os.Bundle);
+    method public abstract android.graphics.Bitmap onGetThumbnail(android.net.Uri, int, int, int);
+    method public abstract java.util.List<android.media.browse.MediaBrowserItem> onLoadChildren(android.net.Uri);
+    method public void setSessionToken(android.media.session.MediaSession.Token);
+    field public static final java.lang.String SERVICE_ACTION = "android.media.browse.MediaBrowserService";
+  }
+
+}
+
 package android.media.effect {
 
   public abstract class Effect {
@@ -16745,9 +16818,12 @@
     field public static final java.lang.String COLUMN_CHANNEL_ID = "channel_id";
     field public static final java.lang.String COLUMN_CONTENT_RATING = "content_rating";
     field public static final java.lang.String COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis";
+    field public static final java.lang.String COLUMN_EPISODE_NUMBER = "episode_number";
+    field public static final java.lang.String COLUMN_EPISODE_TITLE = "episode_title";
     field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_DATA = "internal_provider_data";
     field public static final java.lang.String COLUMN_LONG_DESCRIPTION = "long_description";
     field public static final java.lang.String COLUMN_POSTER_ART_URI = "poster_art_uri";
+    field public static final java.lang.String COLUMN_SEASON_NUMBER = "season_number";
     field public static final java.lang.String COLUMN_SHORT_DESCRIPTION = "short_description";
     field public static final java.lang.String COLUMN_START_TIME_UTC_MILLIS = "start_time_utc_millis";
     field public static final java.lang.String COLUMN_THUMBNAIL_URI = "thumbnail_uri";
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 990ea85..caadecb 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -94,6 +94,13 @@
      */
     public static final int MODE_ERRORED = 2;
 
+    /**
+     * Result from {@link #checkOp}, {@link #noteOp}, {@link #startOp}: the given caller should
+     * use its default security check.  This mode is not normally used; it should only be used
+     * with appop permissions, and callers must explicitly check for it and deal with it.
+     */
+    public static final int MODE_DEFAULT = 3;
+
     // when adding one of these:
     //  - increment _NUM_OP
     //  - add rows to sOpToSwitch, sOpToString, sOpNames, sOpPerms, sOpDefaultMode
@@ -588,7 +595,7 @@
             AppOpsManager.MODE_ALLOWED,
             AppOpsManager.MODE_ALLOWED,
             AppOpsManager.MODE_ALLOWED,
-            AppOpsManager.MODE_IGNORED, // OP_GET_USAGE_STATS
+            AppOpsManager.MODE_DEFAULT, // OP_GET_USAGE_STATS
             AppOpsManager.MODE_ALLOWED,
             AppOpsManager.MODE_ALLOWED,
             AppOpsManager.MODE_IGNORED, // OP_PROJECT_MEDIA
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index 5e55ba7..4b339a1 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -110,6 +110,8 @@
 
     int getFlagsForUid(int uid);
 
+    String[] getAppOpPermissionPackages(String permissionName);
+
     ResolveInfo resolveIntent(in Intent intent, String resolvedType, int flags, int userId);
 
     boolean canForwardTo(in Intent intent, String resolvedType, int sourceUserId, int targetUserId);
diff --git a/core/java/android/content/pm/PermissionInfo.java b/core/java/android/content/pm/PermissionInfo.java
index 5a63e5f..af574db 100644
--- a/core/java/android/content/pm/PermissionInfo.java
+++ b/core/java/android/content/pm/PermissionInfo.java
@@ -69,6 +69,13 @@
     public static final int PROTECTION_FLAG_DEVELOPMENT = 0x20;
 
     /**
+     * Additional flag for {@link #protectionLevel}, corresponding
+     * to the <code>development</code> value of
+     * {@link android.R.attr#protectionLevel}.
+     */
+    public static final int PROTECTION_FLAG_APPOP = 0x40;
+
+    /**
      * Mask for {@link #protectionLevel}: the basic protection type.
      */
     public static final int PROTECTION_MASK_BASE = 0xf;
@@ -153,6 +160,9 @@
         if ((level&PermissionInfo.PROTECTION_FLAG_DEVELOPMENT) != 0) {
             protLevel += "|development";
         }
+        if ((level&PermissionInfo.PROTECTION_FLAG_APPOP) != 0) {
+            protLevel += "|appop";
+        }
         return protLevel;
     }
 
diff --git a/core/java/android/hardware/hdmi/HdmiCecDeviceInfo.java b/core/java/android/hardware/hdmi/HdmiCecDeviceInfo.java
index ae0bda1..acf92f1 100644
--- a/core/java/android/hardware/hdmi/HdmiCecDeviceInfo.java
+++ b/core/java/android/hardware/hdmi/HdmiCecDeviceInfo.java
@@ -57,7 +57,13 @@
     // Value indicating the device is not an active source.
     public static final int DEVICE_INACTIVE = -1;
 
-    // Logical address, phsical address, device type, vendor id and display name
+    /**
+     * Logical address used to indicate the source comes from internal device.
+     * The logical address of TV(0) is used.
+     */
+    public static final int ADDR_INTERNAL = 0;
+
+    // Logical address, physical address, device type, vendor id and display name
     // are immutable value.
     private final int mLogicalAddress;
     private final int mPhysicalAddress;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index a2afb44..7f97726 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2461,7 +2461,7 @@
     <permission android:name="android.permission.PACKAGE_USAGE_STATS"
         android:label="@string/permlab_pkgUsageStats"
         android:description="@string/permdesc_pkgUsageStats"
-        android:protectionLevel="signature|system" />
+        android:protectionLevel="signature|system|development|appop" />
     <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
 
     <!-- @SystemApi Allows an application to collect battery statistics -->
@@ -2469,7 +2469,7 @@
         android:permissionGroup="android.permission-group.SYSTEM_TOOLS"
         android:label="@string/permlab_batteryStats"
         android:description="@string/permdesc_batteryStats"
-        android:protectionLevel="signature|system" />
+        android:protectionLevel="signature|system|development" />
 
     <!-- @SystemApi Allows an application to control the backup and restore process.
     <p>Not for use by third-party applications.
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 3026514..7311a60 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -212,6 +212,9 @@
         <!-- Additional flag from base permission type: this permission can also
              (optionally) be granted to development applications. -->
         <flag name="development" value="0x20" />
+        <!-- Additional flag from base permission type: this permission is closely
+             associated with an app op for controlling access. -->
+        <flag name="appop" value="0x40" />
     </attr>
 
     <!-- Flags indicating more context for a permission group. -->
diff --git a/media/java/android/media/browse/IMediaBrowserService.aidl b/media/java/android/media/browse/IMediaBrowserService.aidl
new file mode 100644
index 0000000..4b2cb9d
--- /dev/null
+++ b/media/java/android/media/browse/IMediaBrowserService.aidl
@@ -0,0 +1,20 @@
+// Copyright 2014 Google Inc. All Rights Reserved.
+
+package android.media.browse;
+
+import android.media.browse.IMediaBrowserServiceCallbacks;
+import android.net.Uri;
+import android.os.Bundle;
+
+/**
+ * Media API allows clients to browse through hierarchy of a user’s media collection,
+ * playback a specific media entry and interact with the now playing queue.
+ * @hide
+ */
+oneway interface IMediaBrowserService {
+    void connect(String pkg, in Bundle rootHints, IMediaBrowserServiceCallbacks callbacks);
+    void disconnect(IMediaBrowserServiceCallbacks callbacks);
+
+    void addSubscription(in Uri uri, IMediaBrowserServiceCallbacks callbacks);
+    void removeSubscription(in Uri uri, IMediaBrowserServiceCallbacks callbacks);
+}
\ No newline at end of file
diff --git a/media/java/android/media/browse/IMediaBrowserServiceCallbacks.aidl b/media/java/android/media/browse/IMediaBrowserServiceCallbacks.aidl
new file mode 100644
index 0000000..ead7624
--- /dev/null
+++ b/media/java/android/media/browse/IMediaBrowserServiceCallbacks.aidl
@@ -0,0 +1,24 @@
+// Copyright 2014 Google Inc. All Rights Reserved.
+
+package android.media.browse;
+
+import android.content.pm.ParceledListSlice;
+import android.media.session.MediaSession;
+import android.net.Uri;
+
+/**
+ * Media API allows clients to browse through hierarchy of a user’s media collection,
+ * playback a specific media entry and interact with the now playing queue.
+ * @hide
+ */
+oneway interface IMediaBrowserServiceCallbacks {
+    /**
+     * Invoked when the connected has been established.
+     * @param root The root Uri for browsing.
+     * @param session The {@link MediaSession.Token media session token} that can be used to control
+     * the playback of the media app.
+     */
+    void onConnect(in Uri root, in MediaSession.Token session);
+    void onConnectFailed();
+    void onLoadChildren(in Uri uri, in ParceledListSlice list);
+}
diff --git a/media/java/android/media/browse/MediaBrowser.java b/media/java/android/media/browse/MediaBrowser.java
new file mode 100644
index 0000000..beec5f9
--- /dev/null
+++ b/media/java/android/media/browse/MediaBrowser.java
@@ -0,0 +1,704 @@
+/*
+ * 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 android.media.browse;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.ParceledListSlice;
+import android.graphics.Bitmap;
+import android.media.session.MediaSession;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Browses media content offered by a link MediaBrowserService.
+ * <p>
+ * This object is not thread-safe. All calls should happen on the thread on which the browser
+ * was constructed.
+ * </p>
+ */
+public final class MediaBrowser {
+    private static final String TAG = "MediaBrowser";
+    private static final boolean DBG = false;
+
+    private static final int CONNECT_STATE_DISCONNECTED = 0;
+    private static final int CONNECT_STATE_CONNECTING = 1;
+    private static final int CONNECT_STATE_CONNECTED = 2;
+    private static final int CONNECT_STATE_SUSPENDED = 3;
+
+    private final Context mContext;
+    private final ComponentName mServiceComponent;
+    private final ConnectionCallback mCallback;
+    private final Bundle mRootHints;
+    private final Handler mHandler = new Handler();
+    private final ArrayMap<Uri,Subscription> mSubscriptions =
+            new ArrayMap<Uri, MediaBrowser.Subscription>();
+
+    private int mState = CONNECT_STATE_DISCONNECTED;
+    private MediaServiceConnection mServiceConnection;
+    private IMediaBrowserService mServiceBinder;
+    private IMediaBrowserServiceCallbacks mServiceCallbacks;
+    private Uri mRootUri;
+    private MediaSession.Token mMediaSessionToken;
+
+    /**
+     * Creates a media browser for the specified media browse service.
+     *
+     * @param context The context.
+     * @param serviceComponent The component name of the media browse service.
+     * @param callback The connection callback.
+     * @param rootHints An optional bundle of service-specific arguments to send
+     * to the media browse service when connecting and retrieving the root uri
+     * for browsing, or null if none.  The contents of this bundle may affect
+     * the information returned when browsing.
+     */
+    public MediaBrowser(Context context, ComponentName serviceComponent,
+            ConnectionCallback callback, Bundle rootHints) {
+        if (context == null) {
+            throw new IllegalArgumentException("context must not be null");
+        }
+        if (serviceComponent == null) {
+            throw new IllegalArgumentException("service component must not be null");
+        }
+        if (callback == null) {
+            throw new IllegalArgumentException("connection callback must not be null");
+        }
+        mContext = context;
+        mServiceComponent = serviceComponent;
+        mCallback = callback;
+        mRootHints = rootHints;
+    }
+
+    /**
+     * Connects to the media browse service.
+     * <p>
+     * The connection callback specified in the constructor will be invoked
+     * when the connection completes or fails.
+     * </p>
+     */
+    public void connect() {
+        if (mState != CONNECT_STATE_DISCONNECTED) {
+            throw new IllegalStateException("connect() called while not disconnected (state="
+                    + getStateLabel(mState) + ")");
+        }
+        // TODO: remove this extra check.
+        if (DBG) {
+            if (mServiceConnection != null) {
+                throw new RuntimeException("mServiceConnection should be null. Instead it is "
+                        + mServiceConnection);
+            }
+        }
+        if (mServiceBinder != null) {
+            throw new RuntimeException("mServiceBinder should be null. Instead it is "
+                    + mServiceBinder);
+        }
+        if (mServiceCallbacks != null) {
+            throw new RuntimeException("mServiceCallbacks should be null. Instead it is "
+                    + mServiceCallbacks);
+        }
+
+        mState = CONNECT_STATE_CONNECTING;
+
+        final Intent intent = new Intent(MediaBrowserService.SERVICE_ACTION);
+        intent.setComponent(mServiceComponent);
+
+        final ServiceConnection thisConnection = mServiceConnection = new MediaServiceConnection();
+
+        try {
+            mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
+        } catch (Exception ex) {
+            Log.e(TAG, "Failed binding to service " + mServiceComponent);
+
+            // Tell them that it didn't work.  We are already on the main thread,
+            // but we don't want to do callbacks inside of connect().  So post it,
+            // and then check that we are on the same ServiceConnection.  We know
+            // we won't also get an onServiceConnected or onServiceDisconnected,
+            // so we won't be doing double callbacks.
+            mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        // Ensure that nobody else came in or tried to connect again.
+                        if (thisConnection == mServiceConnection) {
+                            forceCloseConnection();
+                            mCallback.onConnectionFailed();
+                        }
+                    }
+                });
+        }
+
+        if (DBG) {
+            Log.d(TAG, "connect...");
+            dump();
+        }
+    }
+
+    /**
+     * Disconnects from the media browse service.
+     * @more
+     * After this, no more callbacks will be received.
+     */
+    public void disconnect() {
+        // It's ok to call this any state, because allowing this lets apps not have
+        // to check isConnected() unnecessarily.  They won't appreciate the extra
+        // assertions for this.  We do everything we can here to go back to a sane state.
+        if (mServiceCallbacks != null) {
+            try {
+                mServiceBinder.disconnect(mServiceCallbacks);
+            } catch (RemoteException ex) {
+                // We are disconnecting anyway.  Log, just for posterity but it's not
+                // a big problem.
+                Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
+            }
+        }
+        forceCloseConnection();
+
+        if (DBG) {
+            Log.d(TAG, "disconnect...");
+            dump();
+        }
+    }
+
+    /**
+     * Null out the variables and unbind from the service.  This doesn't include
+     * calling disconnect on the service, because we only try to do that in the
+     * clean shutdown cases.
+     * <p>
+     * Everywhere that calls this EXCEPT for disconnect() should follow it with
+     * a call to mCallback.onConnectionFailed().  Disconnect doesn't do that callback
+     * for a clean shutdown, but everywhere else is a dirty shutdown and should
+     * notify the app.
+     */
+    private void forceCloseConnection() {
+        if (mServiceConnection != null) {
+            mContext.unbindService(mServiceConnection);
+        }
+        mState = CONNECT_STATE_DISCONNECTED;
+        mServiceConnection = null;
+        mServiceBinder = null;
+        mServiceCallbacks = null;
+        mRootUri = null;
+        mMediaSessionToken = null;
+    }
+
+    /**
+     * Returns whether the browser is connected to the service.
+     */
+    public boolean isConnected() {
+        return mState == CONNECT_STATE_CONNECTED;
+    }
+
+    /**
+     * Gets the root Uri.
+     * <p>
+     * Note that the root uri may become invalid or change when when the
+     * browser is disconnected.
+     * </p>
+     *
+     * @throws IllegalStateException if not connected.
+      */
+    public @NonNull Uri getRoot() {
+        if (mState != CONNECT_STATE_CONNECTED) {
+            throw new IllegalStateException("getSessionToken() called while not connected (state="
+                    + getStateLabel(mState) + ")");
+        }
+        return mRootUri;
+    }
+
+    /**
+     * Gets the media session token associated with the media browser.
+     * <p>
+     * Note that the session token may become invalid or change when when the
+     * browser is disconnected.
+     * </p>
+     *
+     * @return The session token for the browser, never null.
+     *
+     * @throws IllegalStateException if not connected.
+     */
+     public @NonNull MediaSession.Token getSessionToken() {
+        if (mState != CONNECT_STATE_CONNECTED) {
+            throw new IllegalStateException("getSessionToken() called while not connected (state="
+                    + mState + ")");
+        }
+        return mMediaSessionToken;
+    }
+
+    /**
+     * Queries for information about the media items that are contained within
+     * the specified Uri and subscribes to receive updates when they change.
+     * <p>
+     * The list of subscriptions is maintained even when not connected and is
+     * restored after reconnection.  It is ok to subscribe while not connected
+     * but the results will not be returned until the connection completes.
+     * </p><p>
+     * If the uri is already subscribed with a different callback then the new
+     * callback will replace the previous one.
+     * </p>
+     *
+     * @param parentUri The uri of the parent media item whose list of children
+     * will be subscribed.
+     * @param callback The callback to receive the list of children.
+     */
+    public void subscribe(@NonNull Uri parentUri, @NonNull SubscriptionCallback callback) {
+        // Check arguments.
+        if (parentUri == null) {
+            throw new IllegalArgumentException("parentUri is null");
+        }
+        if (callback == null) {
+            throw new IllegalArgumentException("callback is null");
+        }
+
+        // Update or create the subscription.
+        Subscription sub = mSubscriptions.get(parentUri);
+        boolean newSubscription = sub == null;
+        if (newSubscription) {
+            sub = new Subscription(parentUri);
+            mSubscriptions.put(parentUri, sub);
+        }
+        sub.callback = callback;
+
+        // If we are connected, tell the service that we are watching.  If we aren't
+        // connected, the service will be told when we connect.
+        if (mState == CONNECT_STATE_CONNECTED && newSubscription) {
+            try {
+                mServiceBinder.addSubscription(parentUri, mServiceCallbacks);
+            } catch (RemoteException ex) {
+                // Process is crashing.  We will disconnect, and upon reconnect we will
+                // automatically reregister. So nothing to do here.
+                Log.d(TAG, "addSubscription failed with RemoteException parentUri=" + parentUri);
+            }
+        }
+    }
+
+    /**
+     * Unsubscribes for changes to the children of the specified Uri.
+     * <p>
+     * The query callback will no longer be invoked for results associated with
+     * this Uri once this method returns.
+     * </p>
+     *
+     * @param parentUri The uri of the parent media item whose list of children
+     * will be unsubscribed.
+     */
+    public void unsubscribe(@NonNull Uri parentUri) {
+        // Check arguments.
+        if (parentUri == null) {
+            throw new IllegalArgumentException("parentUri is null");
+        }
+
+        // Remove from our list.
+        final Subscription sub = mSubscriptions.remove(parentUri);
+
+        // Tell the service if necessary.
+        if (mState == CONNECT_STATE_CONNECTED && sub != null) {
+            try {
+                mServiceBinder.removeSubscription(parentUri, mServiceCallbacks);
+            } catch (RemoteException ex) {
+                // Process is crashing.  We will disconnect, and upon reconnect we will
+                // automatically reregister. So nothing to do here.
+                Log.d(TAG, "removeSubscription failed with RemoteException parentUri=" + parentUri);
+            }
+        }
+    }
+
+    /**
+     * Loads the thumbnail of a media item.
+     *
+     * @param uri The uri of the media item.
+     * @param width The preferred width of the icon in dp.
+     * @param height The preferred width of the icon in dp.
+     * @param density The preferred density of the icon. Must be one of the android
+     *      density buckets.
+     * @param callback The callback to receive the thumbnail.
+     *
+     * @throws IllegalStateException if not connected. TODO: Is this restriction necessary?
+     */
+    public void loadThumbnail(@NonNull Uri uri, int width, int height, int density,
+            @NonNull ThumbnailCallback callback) {
+        throw new RuntimeException("implement me");
+    }
+
+    /**
+     * For debugging.
+     */
+    private static String getStateLabel(int state) {
+        switch (state) {
+            case CONNECT_STATE_DISCONNECTED:
+                return "CONNECT_STATE_DISCONNECTED";
+            case CONNECT_STATE_CONNECTING:
+                return "CONNECT_STATE_CONNECTING";
+            case CONNECT_STATE_CONNECTED:
+                return "CONNECT_STATE_CONNECTED";
+            case CONNECT_STATE_SUSPENDED:
+                return "CONNECT_STATE_SUSPENDED";
+            default:
+                return "UNKNOWN/" + state;
+        }
+    }
+
+    private final void onServiceConnected(final IMediaBrowserServiceCallbacks callback,
+            final Uri root, final MediaSession.Token session) {
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                // Check to make sure there hasn't been a disconnect or a different
+                // ServiceConnection.
+                if (!isCurrent(callback, "onConnect")) {
+                    return;
+                }
+                // Don't allow them to call us twice.
+                if (mState != CONNECT_STATE_CONNECTING) {
+                    Log.w(TAG, "onConnect from service while mState="
+                            + getStateLabel(mState) + "... ignoring");
+                    return;
+                }
+                mRootUri = root;
+                mMediaSessionToken = session;
+                mState = CONNECT_STATE_CONNECTED;
+
+                if (DBG) {
+                    Log.d(TAG, "ServiceCallbacks.onConnect...");
+                    dump();
+                }
+                mCallback.onConnected();
+
+                // we may receive some subscriptions before we are connected, so re-subscribe
+                // everything now
+                for (Uri uri : mSubscriptions.keySet()) {
+                    try {
+                        mServiceBinder.addSubscription(uri, mServiceCallbacks);
+                    } catch (RemoteException ex) {
+                        // Process is crashing.  We will disconnect, and upon reconnect we will
+                        // automatically reregister. So nothing to do here.
+                        Log.d(TAG, "addSubscription failed with RemoteException parentUri=" + uri);
+                    }
+                }
+
+            }
+        });
+    }
+
+    private final void onConnectionFailed(final IMediaBrowserServiceCallbacks callback) {
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                Log.e(TAG, "onConnectFailed for " + mServiceComponent);
+
+                // Check to make sure there hasn't been a disconnect or a different
+                // ServiceConnection.
+                if (!isCurrent(callback, "onConnectFailed")) {
+                    return;
+                }
+                // Don't allow them to call us twice.
+                if (mState != CONNECT_STATE_CONNECTING) {
+                    Log.w(TAG, "onConnect from service while mState="
+                            + getStateLabel(mState) + "... ignoring");
+                    return;
+                }
+
+                // Clean up
+                forceCloseConnection();
+
+                // Tell the app.
+                mCallback.onConnectionFailed();
+            }
+        });
+    }
+
+    private final void onLoadChildren(final IMediaBrowserServiceCallbacks callback, final Uri uri,
+            final ParceledListSlice list) {
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                // Check that there hasn't been a disconnect or a different
+                // ServiceConnection.
+                if (!isCurrent(callback, "onLoadChildren")) {
+                    return;
+                }
+
+                List<MediaBrowserItem> data = list.getList();
+                if (DBG) {
+                    Log.d(TAG, "onLoadChildren for " + mServiceComponent + " uri=" + uri);
+                }
+                if (data == null) {
+                    data = Collections.emptyList();
+                }
+
+                // Check that the subscription is still subscribed.
+                final Subscription subscription = mSubscriptions.get(uri);
+                if (subscription == null) {
+                    if (DBG) {
+                        Log.d(TAG, "onLoadChildren for uri that isn't subscribed uri="
+                                + uri);
+                    }
+                    return;
+                }
+
+                // Tell the app.
+                subscription.callback.onChildrenLoaded(uri, data);
+            }
+        });
+    }
+
+
+    /**
+     * Return true if {@code callback} is the current ServiceCallbacks.  Also logs if it's not.
+     */
+    private boolean isCurrent(IMediaBrowserServiceCallbacks callback, String funcName) {
+        if (mServiceCallbacks != callback) {
+            if (mState != CONNECT_STATE_DISCONNECTED) {
+                Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection="
+                        + mServiceCallbacks + " this=" + this);
+            }
+            return false;
+        }
+        return true;
+    }
+
+    private ServiceCallbacks getNewServiceCallbacks() {
+        return new ServiceCallbacks(this);
+    }
+
+    /**
+     * Log internal state.
+     * @hide
+     */
+    void dump() {
+        Log.d(TAG, "MediaBrowser...");
+        Log.d(TAG, "  mServiceComponent=" + mServiceComponent);
+        Log.d(TAG, "  mCallback=" + mCallback);
+        Log.d(TAG, "  mRootHints=" + mRootHints);
+        Log.d(TAG, "  mState=" + getStateLabel(mState));
+        Log.d(TAG, "  mServiceConnection=" + mServiceConnection);
+        Log.d(TAG, "  mServiceBinder=" + mServiceBinder);
+        Log.d(TAG, "  mServiceCallbacks=" + mServiceCallbacks);
+        Log.d(TAG, "  mRootUri=" + mRootUri);
+        Log.d(TAG, "  mMediaSessionToken=" + mMediaSessionToken);
+    }
+
+
+    /**
+     * Callbacks for connection related events.
+     */
+    public static class ConnectionCallback {
+        /**
+         * Invoked after {@link MediaBrowser#connect()} when the request has successfully completed.
+         */
+        public void onConnected() {
+        }
+
+        /**
+         * Invoked when the client is disconnected from the media browser.
+         */
+        public void onConnectionSuspended() {
+        }
+
+        /**
+         * Invoked when the connection to the media browser failed.
+         */
+        public void onConnectionFailed() {
+        }
+    }
+
+    /**
+     * Callbacks for subscription related events.
+     */
+    public static abstract class SubscriptionCallback {
+        /**
+         * Called when the list of children is loaded or updated.
+         */
+        public void onChildrenLoaded(@NonNull Uri parentUri,
+                                     @NonNull List<MediaBrowserItem> children) {
+        }
+
+        /**
+         * Called when the Uri doesn't exist or other errors in subscribing.
+         * <p>
+         * If this is called, the subscription remains until {@link MediaBrowser#unsubscribe}
+         * called, because some errors may heal themselves.
+         * </p>
+         */
+        public void onError(@NonNull Uri uri) {
+        }
+    }
+
+    /**
+     * Callbacks for thumbnail loading.
+     */
+    public static abstract class ThumbnailCallback {
+        /**
+         * Called when the thumbnail is loaded.
+         */
+        public void onThumbnailLoaded(@NonNull Uri uri, @NonNull Bitmap bitmap) {
+        }
+
+        /**
+         * Called when the Uri doesn’t exist or the bitmap cannot be loaded.
+         */
+        public void onError(@NonNull Uri uri) {
+        }
+    }
+
+    /**
+     * ServiceConnection to the other app.
+     */
+    private class MediaServiceConnection implements ServiceConnection {
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder binder) {
+            if (DBG) {
+                Log.d(TAG, "MediaServiceConnection.onServiceConnected name=" + name
+                        + " binder=" + binder);
+                dump();
+            }
+
+            // Make sure we are still the current connection, and that they haven't called
+            // disconnect().
+            if (!isCurrent("onServiceConnected")) {
+                return;
+            }
+
+            // Save their binder
+            mServiceBinder = IMediaBrowserService.Stub.asInterface(binder);
+
+            // We make a new mServiceCallbacks each time we connect so that we can drop
+            // responses from previous connections.
+            mServiceCallbacks = getNewServiceCallbacks();
+
+            // Call connect, which is async. When we get a response from that we will
+            // say that we're connected.
+            try {
+                if (DBG) {
+                    Log.d(TAG, "ServiceCallbacks.onConnect...");
+                    dump();
+                }
+                mServiceBinder.connect(mContext.getPackageName(), mRootHints, mServiceCallbacks);
+            } catch (RemoteException ex) {
+                // Connect failed, which isn't good. But the auto-reconnect on the service
+                // will take over and we will come back.  We will also get the
+                // onServiceDisconnected, which has all the cleanup code.  So let that do it.
+                Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
+                if (DBG) {
+                    Log.d(TAG, "ServiceCallbacks.onConnect...");
+                    dump();
+                }
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            if (DBG) {
+                Log.d(TAG, "MediaServiceConnection.onServiceDisconnected name=" + name
+                        + " this=" + this + " mServiceConnection=" + mServiceConnection);
+                dump();
+            }
+
+            // Make sure we are still the current connection, and that they haven't called
+            // disconnect().
+            if (!isCurrent("onServiceDisconnected")) {
+                return;
+            }
+
+            // Clear out what we set in onServiceConnected
+            mServiceBinder = null;
+            mServiceCallbacks = null;
+
+            // And tell the app that it's suspended.
+            mState = CONNECT_STATE_SUSPENDED;
+            mCallback.onConnectionSuspended();
+        }
+
+        /**
+         * Return true if this is the current ServiceConnection.  Also logs if it's not.
+         */
+        private boolean isCurrent(String funcName) {
+            if (mServiceConnection != this) {
+                if (mState != CONNECT_STATE_DISCONNECTED) {
+                    // Check mState, because otherwise this log is noisy.
+                    Log.i(TAG, funcName + " for " + mServiceComponent + " with mServiceConnection="
+                            + mServiceConnection + " this=" + this);
+                }
+                return false;
+            }
+            return true;
+        }
+    };
+
+    /**
+     * Callbacks from the service.
+     */
+    private static class ServiceCallbacks extends IMediaBrowserServiceCallbacks.Stub {
+        private WeakReference<MediaBrowser> mMediaBrowser;
+
+        public ServiceCallbacks(MediaBrowser mediaBrowser) {
+            mMediaBrowser = new WeakReference<MediaBrowser>(mediaBrowser);
+        }
+
+        /**
+         * The other side has acknowledged our connection.  The parameters to this function
+         * are the initial data as requested.
+         */
+        @Override
+        public void onConnect(final Uri root, final MediaSession.Token session) {
+            MediaBrowser mediaBrowser = mMediaBrowser.get();
+            if (mediaBrowser != null) {
+                mediaBrowser.onServiceConnected(this, root, session);
+            }
+        }
+
+        /**
+         * The other side does not like us.  Tell the app via onConnectionFailed.
+         */
+        @Override
+        public void onConnectFailed() {
+            MediaBrowser mediaBrowser = mMediaBrowser.get();
+            if (mediaBrowser != null) {
+                mediaBrowser.onConnectionFailed(this);
+            }
+        }
+
+        @Override
+        public void onLoadChildren(final Uri uri, final ParceledListSlice list) {
+            MediaBrowser mediaBrowser = mMediaBrowser.get();
+            if (mediaBrowser != null) {
+                mediaBrowser.onLoadChildren(this, uri, list);
+            }
+        }
+    }
+
+    private static class Subscription {
+        final Uri uri;
+        SubscriptionCallback callback;
+
+        Subscription(Uri u) {
+            this.uri = u;
+        }
+    }
+}
diff --git a/media/java/android/media/browse/MediaBrowserItem.java b/media/java/android/media/browse/MediaBrowserItem.java
new file mode 100644
index 0000000..119f687
--- /dev/null
+++ b/media/java/android/media/browse/MediaBrowserItem.java
@@ -0,0 +1,241 @@
+/*
+ * 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 android.media.browse;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.net.Uri;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Describes a media item in the list of items offered by a {@link MediaBrowserService}.
+ */
+public final class MediaBrowserItem implements Parcelable {
+    private final Uri mUri;
+    private final int mFlags;
+    private final CharSequence mTitle;
+    private final CharSequence mSummary;
+    private final Bundle mExtras;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag=true, value = { FLAG_BROWSABLE, FLAG_PLAYABLE })
+    public @interface Flags { }
+
+    /**
+     * Flag: Indicates that the item has children of its own.
+     */
+    public static final int FLAG_BROWSABLE = 1 << 0;
+
+    /**
+     * Flag: Indicates that the item is playable.
+     * <p>
+     * The Uri of this item may be passed to link android.media.session.MediaController#play(Uri)
+     * to start playing it.
+     * </p>
+     */
+    public static final int FLAG_PLAYABLE = 1 << 1;
+
+    /**
+     * Initialize a MediaBrowserItem object.
+     */
+    private MediaBrowserItem(@NonNull Uri uri, int flags, @NonNull CharSequence title,
+            CharSequence summary, Bundle extras) {
+        if (uri == null) {
+            throw new IllegalArgumentException("uri can not be null");
+        }
+        if (title == null) {
+            throw new IllegalArgumentException("title can not be null");
+        }
+        mUri = uri;
+        mFlags = flags;
+        mTitle = title;
+        mSummary = summary;
+        mExtras = extras;
+    }
+
+    /**
+     * Private constructor.
+     */
+    private MediaBrowserItem(Parcel in) {
+        mUri = Uri.CREATOR.createFromParcel(in);
+        mFlags = in.readInt();
+        mTitle = in.readCharSequence();
+        if (in.readInt() != 0) {
+            mSummary = in.readCharSequence();
+        } else {
+            mSummary = null;
+        }
+        if (in.readInt() != 0) {
+            mExtras = Bundle.CREATOR.createFromParcel(in);
+        } else {
+            mExtras = null;
+        }
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        mUri.writeToParcel(out, flags);
+        out.writeInt(mFlags);
+        out.writeCharSequence(mTitle);
+        if (mSummary != null) {
+            out.writeInt(1);
+            out.writeCharSequence(mSummary);
+        } else {
+            out.writeInt(0);
+        }
+        if (mExtras != null) {
+            out.writeInt(1);
+            mExtras.writeToParcel(out, flags);
+        } else {
+            out.writeInt(0);
+        }
+    }
+
+    public static final Parcelable.Creator<MediaBrowserItem> CREATOR =
+            new Parcelable.Creator<MediaBrowserItem>() {
+        @Override
+        public MediaBrowserItem createFromParcel(Parcel in) {
+            return new MediaBrowserItem(in);
+        }
+
+        @Override
+        public MediaBrowserItem[] newArray(int size) {
+            return new MediaBrowserItem[size];
+        }
+    };
+
+    /**
+     * Gets the Uri of the item.
+     */
+    public @NonNull Uri getUri() {
+        return mUri;
+    }
+
+    /**
+     * Gets the flags of the item.
+     */
+    public @Flags int getFlags() {
+        return mFlags;
+    }
+
+    /**
+     * Returns whether this item is browsable.
+     * @see #FLAG_BROWSABLE
+     */
+    public boolean isBrowsable() {
+        return (mFlags & FLAG_BROWSABLE) != 0;
+    }
+
+    /**
+     * Returns whether this item is playable.
+     * @see #FLAG_PLAYABLE
+     */
+    public boolean isPlayable() {
+        return (mFlags & FLAG_PLAYABLE) != 0;
+    }
+
+    /**
+     * Gets the title of the item.
+     * @more
+     * The title will be shown as the first line of text when
+     * describing each item to the user.
+     */
+    public @NonNull CharSequence getTitle() {
+        return mTitle;
+    }
+
+    /**
+     * Gets summary of the item, or null if none.
+     * @more
+     * The summary will be shown as the second line of text when
+     * describing each item to the user.
+     */
+    public @Nullable CharSequence getSummary() {
+        return mSummary;
+    }
+
+    /**
+     * Gets additional service-specified extras about the
+     * item or its content, or null if none.
+     */
+    public @Nullable Bundle getExtras() {
+        return mExtras;
+    }
+
+    /**
+     * Builder for {@link MediaBrowserItem} objects.
+     */
+    public static final class Builder {
+        private final Uri mUri;
+        private final int mFlags;
+        private final CharSequence mTitle;
+        private CharSequence mSummary;
+        private Bundle mExtras;
+
+        /**
+         * Creates an item builder.
+         */
+        public Builder(@NonNull Uri uri, @Flags int flags, @NonNull CharSequence title) {
+            if (uri == null) {
+                throw new IllegalArgumentException("uri can not be null");
+            }
+            if (title == null) {
+                throw new IllegalArgumentException("title can not be null");
+            }
+            mUri = uri;
+            mFlags = flags;
+            mTitle = title;
+        }
+
+        /**
+         * Sets summary of the item, or null if none.
+         */
+        public @NonNull Builder setSummary(@Nullable CharSequence summary) {
+            mSummary = summary;
+            return this;
+        }
+
+        /**
+        * Sets additional service-specified extras about the
+        * item or its content, or null if none.
+        */
+        public @NonNull Builder setExtras(@Nullable Bundle extras) {
+            mExtras = extras;
+            return this;
+        }
+
+        /**
+        * Builds the item.
+        */
+        public @NonNull MediaBrowserItem build() {
+            return new MediaBrowserItem(mUri, mFlags, mTitle, mSummary, mExtras);
+        }
+    }
+}
+
diff --git a/media/java/android/media/browse/MediaBrowserService.java b/media/java/android/media/browse/MediaBrowserService.java
new file mode 100644
index 0000000..ceb4b03
--- /dev/null
+++ b/media/java/android/media/browse/MediaBrowserService.java
@@ -0,0 +1,363 @@
+/*
+ * 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 android.media.browse;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.app.Service;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
+import android.graphics.Bitmap;
+import android.media.session.MediaSession;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Base class for media browse services.
+ * <p>
+ * Media browse services enable applications to browse media content provided by an application
+ * and ask the application to start playing it.  They may also be used to control content that
+ * is already playing by way of a {@link MediaSession}.
+ * </p>
+ *
+ * To extend this class, you must declare the service in your manifest file with
+ * an intent filter with the {@link #SERVICE_ACTION} action.
+ *
+ * For example:
+ * </p><pre>
+ * &lt;service android:name=".MyMediaBrowserService"
+ *          android:label="&#64;string/service_name" >
+ *     &lt;intent-filter>
+ *         &lt;action android:name="android.media.browse.MediaBrowserService" />
+ *     &lt;/intent-filter>
+ * &lt;/service>
+ * </pre>
+ *
+ */
+public abstract class MediaBrowserService extends Service {
+    private static final String TAG = "MediaBrowserService";
+
+    private final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap();
+    private final Handler mHandler = new Handler();
+    private ServiceBinder mBinder;
+    MediaSession.Token mSession;
+
+    /**
+     * All the info about a connection.
+     */
+    private class ConnectionRecord {
+        String pkg;
+        Bundle rootHints;
+        IMediaBrowserServiceCallbacks callbacks;
+        Uri root;
+        HashSet<Uri> subscriptions = new HashSet();
+    }
+
+    /**
+     * The {@link Intent} that must be declared as handled by the service.
+     */
+    @SdkConstant(SdkConstantType.SERVICE_ACTION)
+    public static final String SERVICE_ACTION = "android.media.browse.MediaBrowserService";
+
+    private class ServiceBinder extends IMediaBrowserService.Stub {
+        @Override
+        public void connect(final String pkg, final Bundle rootHints,
+                final IMediaBrowserServiceCallbacks callbacks) {
+
+            final int uid = Binder.getCallingUid();
+            if (!isValidPackage(pkg, uid)) {
+                throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid
+                        + " package=" + pkg);
+            }
+
+            mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        final IBinder b = callbacks.asBinder();
+
+                        // Clear out the old subscriptions.  We are getting new ones.
+                        mConnections.remove(b);
+
+                        final ConnectionRecord connection = new ConnectionRecord();
+                        connection.pkg = pkg;
+                        connection.rootHints = rootHints;
+                        connection.callbacks = callbacks;
+
+                        connection.root = MediaBrowserService.this.onGetRoot(pkg, uid, rootHints);
+
+                        // If they didn't return something, don't allow this client.
+                        if (connection.root == null) {
+                            Log.i(TAG, "No root for client " + pkg + " from service "
+                                    + getClass().getName());
+                            try {
+                                callbacks.onConnectFailed();
+                            } catch (RemoteException ex) {
+                                Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. "
+                                        + "pkg=" + pkg);
+                            }
+                        } else {
+                            try {
+                                mConnections.put(b, connection);
+                                callbacks.onConnect(connection.root, mSession);
+                            } catch (RemoteException ex) {
+                                Log.w(TAG, "Calling onConnect() failed. Dropping client. "
+                                        + "pkg=" + pkg);
+                                mConnections.remove(b);
+                            }
+                        }
+                    }
+                });
+        }
+
+        @Override
+        public void disconnect(final IMediaBrowserServiceCallbacks callbacks) {
+            mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        final IBinder b = callbacks.asBinder();
+
+                        // Clear out the old subscriptions.  We are getting new ones.
+                        final ConnectionRecord old = mConnections.remove(b);
+                        if (old != null) {
+                            // TODO
+                        }
+                    }
+                });
+        }
+
+
+        @Override
+        public void addSubscription(final Uri uri, final IMediaBrowserServiceCallbacks callbacks) {
+            mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        final IBinder b = callbacks.asBinder();
+
+                        // Get the record for the connection
+                        final ConnectionRecord connection = mConnections.get(b);
+                        if (connection == null) {
+                            Log.w(TAG, "addSubscription for callback that isn't registered uri="
+                                + uri);
+                            return;
+                        }
+
+                        MediaBrowserService.this.addSubscription(uri, connection);
+                    }
+                });
+        }
+
+        @Override
+        public void removeSubscription(final Uri uri,
+                final IMediaBrowserServiceCallbacks callbacks) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    final IBinder b = callbacks.asBinder();
+
+                    ConnectionRecord connection = mConnections.get(b);
+                    if (connection == null) {
+                        Log.w(TAG, "removeSubscription for callback that isn't registered uri="
+                                + uri);
+                        return;
+                    }
+                    if (!connection.subscriptions.remove(uri)) {
+                        Log.w(TAG, "removeSubscription called for " + uri
+                                + " which is not subscribed");
+                    }
+                }
+            });
+        }
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mBinder = new ServiceBinder();
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        if (SERVICE_ACTION.equals(intent.getAction())) {
+            return mBinder;
+        }
+        return null;
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+    }
+
+    /**
+     * Called to get the root uri for browsing by a particular client.
+     * <p>
+     * The implementation should verify that the client package has
+     * permission to access browse media information before returning
+     * the root uri; it should return null if the client is not
+     * allowed to access this information.
+     * </p>
+     *
+     * @param clientPackageName The package name of the application
+     * which is requesting access to browse media.
+     * @param clientUid The uid of the application which is requesting
+     * access to browse media.
+     * @param rootHints An optional bundle of service-specific arguments to send
+     * to the media browse service when connecting and retrieving the root uri
+     * for browsing, or null if none.  The contents of this bundle may affect
+     * the information returned when browsing.
+     */
+    public abstract @Nullable Uri onGetRoot(@NonNull String clientPackageName, int clientUid,
+            @Nullable Bundle rootHints);
+
+    /**
+     * Called to get information about the children of a media item.
+     *
+     * @param parentUri The uri of the parent media item whose
+     * children are to be queried.
+     * @return The list of children, or null if the uri is invalid.
+     */
+    public abstract @Nullable List<MediaBrowserItem> onLoadChildren(@NonNull Uri parentUri);
+
+    /**
+     * Called to get the thumbnail of a particular media item.
+     *
+     * @param uri The uri of the media item.
+     * @param width The requested width of the icon in dp.
+     * @param height The requested height of the icon in dp.
+     * @param density The requested density of the icon. This is the approximate density of the
+     *              screen on which the icon will be displayed.  This density will be one of
+     *              the android density buckets.
+     * @return The file descriptor of the thumbnail, which may then be loaded
+     *          using a bitmap factory, or null if the item does not have a thumbnail.
+     */
+    public abstract @Nullable Bitmap onGetThumbnail(@NonNull Uri uri,
+            int width, int height, int density);
+
+    /**
+     * Call to set the media session.
+     * <p>
+     * This must be called before onCreate returns.
+     *
+     * @return The media session token, must not be null.
+     */
+    public void setSessionToken(MediaSession.Token token) {
+        if (token == null) {
+            throw new IllegalStateException(this.getClass().getName()
+                    + ".onCreateSession() set invalid MediaSession.Token");
+        }
+        mSession = token;
+    }
+
+    /**
+     * Gets the session token, or null if it has not yet been created
+     * or if it has been destroyed.
+     */
+    public @Nullable MediaSession.Token getSessionToken() {
+        return mSession;
+    }
+
+    /**
+     * Notifies all connected media browsers that the content of
+     * the browse service has changed in some way.
+     * This will cause browsers to fetch subscribed content again.
+     */
+    public void notifyChange() {
+        throw new RuntimeException("implement me");
+    }
+
+    /**
+     * Notifies all connected media browsers that the children of
+     * the specified Uri have changed in some way.
+     * This will cause browsers to fetch subscribed content again.
+     *
+     * @param parentUri The uri of the parent media item whose
+     * children changed.
+     */
+    public void notifyChildrenChanged(@NonNull Uri parentUri) {
+        throw new RuntimeException("implement me");
+    }
+
+    /**
+     * Return whether the given package is one of the ones that is owned by the uid.
+     */
+    private boolean isValidPackage(String pkg, int uid) {
+        if (pkg == null) {
+            return false;
+        }
+        final PackageManager pm = getPackageManager();
+        final String[] packages = pm.getPackagesForUid(uid);
+        final int N = packages.length;
+        for (int i=0; i<N; i++) {
+            if (packages[i].equals(pkg)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Save the subscription and if it is a new subscription send the results.
+     */
+    private void addSubscription(Uri uri, ConnectionRecord connection) {
+        // Save the subscription
+        final boolean added = connection.subscriptions.add(uri);
+
+        // If this is a new subscription, send the results
+        if (added) {
+            performLoadChildren(uri, connection);
+        }
+    }
+
+    /**
+     * Call onLoadChildren and then send the results back to the connection.
+     * <p>
+     * Callers must make sure that this connection is still connected.
+     * <p>
+     * TODO: Think about caching and combining these calls.
+     */
+    private void performLoadChildren(Uri uri, ConnectionRecord connection) {
+        final List<MediaBrowserItem> list = onLoadChildren(uri);
+        if (list == null) {
+            throw new IllegalStateException("onLoadChildren returned null for uri " + uri);
+        }
+        final ParceledListSlice<MediaBrowserItem> pls = new ParceledListSlice(list);
+        try {
+            connection.callbacks.onLoadChildren(uri, pls);
+        } catch (RemoteException ex) {
+            // The other side is in the process of crashing.
+            Log.w(TAG, "Calling onLoadChildren() failed for uri=" + uri
+                    + " package=" + connection.pkg);
+        }
+    }
+}
diff --git a/media/java/android/media/tv/TvContract.java b/media/java/android/media/tv/TvContract.java
index 949fc7d..acdfbe19 100644
--- a/media/java/android/media/tv/TvContract.java
+++ b/media/java/android/media/tv/TvContract.java
@@ -815,6 +815,36 @@
         public static final String COLUMN_TITLE = "title";
 
         /**
+         * The season number of this TV program for episodic TV shows.
+         * <p>
+         * Can be empty.
+         * </p><p>
+         * Type: INTEGER
+         * </p>
+         **/
+        public static final String COLUMN_SEASON_NUMBER = "season_number";
+
+        /**
+         * The episode number of this TV program for episodic TV shows.
+         * <p>
+         * Can be empty.
+         * </p><p>
+         * Type: INTEGER
+         * </p>
+         **/
+        public static final String COLUMN_EPISODE_NUMBER = "episode_number";
+
+        /**
+         * The episode title of this TV program for episodic TV shows.
+         * <p>
+         * Can be empty.
+         * </p><p>
+         * Type: TEXT
+         * </p>
+         **/
+        public static final String COLUMN_EPISODE_TITLE = "episode_title";
+
+        /**
          * The start time of this TV program, in milliseconds since the epoch.
          * <p>
          * Type: INTEGER (long)
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 9945909..ca9f6eb 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -185,8 +185,8 @@
 public class ConnectivityService extends IConnectivityManager.Stub {
     private static final String TAG = "ConnectivityService";
 
-    private static final boolean DBG = true;
-    private static final boolean VDBG = true; // STOPSHIP
+    private static final boolean DBG = false;
+    private static final boolean VDBG = false; // STOPSHIP
 
     // network sampling debugging
     private static final boolean SAMPLE_DBG = false;
diff --git a/services/core/java/com/android/server/hdmi/Constants.java b/services/core/java/com/android/server/hdmi/Constants.java
index 85c7747..946d4ce 100644
--- a/services/core/java/com/android/server/hdmi/Constants.java
+++ b/services/core/java/com/android/server/hdmi/Constants.java
@@ -16,6 +16,8 @@
 
 package com.android.server.hdmi;
 
+import android.hardware.hdmi.HdmiCecDeviceInfo;
+
 /**
  * Defines constants related to HDMI-CEC protocol internal implementation.
  * If a constant will be used in the public api, it should be located in
@@ -78,7 +80,7 @@
     public static final int ADDR_INVALID = -1;
 
     /** Logical address used to indicate the source comes from internal device. */
-    public static final int ADDR_INTERNAL = 0xFFFF;
+    public static final int ADDR_INTERNAL = HdmiCecDeviceInfo.ADDR_INTERNAL;
 
     static final int MESSAGE_FEATURE_ABORT = 0x00;
     static final int MESSAGE_IMAGE_VIEW_ON = 0x04;
@@ -179,10 +181,11 @@
     static final int INVALID_PORT_ID = -1;
     static final int INVALID_PHYSICAL_ADDRESS = 0xFFFF;
 
-    // Send result codes.
+    // Send result codes. It should be consistent with hdmi_cec.h's send_message error code.
     static final int SEND_RESULT_SUCCESS = 0;
-    static final int SEND_RESULT_NAK = -1;
-    static final int SEND_RESULT_FAILURE = -2;
+    static final int SEND_RESULT_NAK = 1;
+    static final int SEND_RESULT_BUSY = 2;
+    static final int SEND_RESULT_FAILURE = 3;
 
     // Strategy for device polling.
     // Should use "OR(|) operation of POLL_STRATEGY_XXX and POLL_ITERATION_XXX.
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index a66f473..e985e35 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -556,6 +556,9 @@
     @ServiceThreadOnly
     private void clearDeviceInfoList() {
         assertRunOnServiceThread();
+        for (HdmiCecDeviceInfo info : mSafeExternalInputs) {
+            mService.invokeDeviceEventListeners(info, false);
+        }
         mDeviceInfos.clear();
         updateSafeDeviceInfoList();
     }
@@ -1129,6 +1132,7 @@
 
         disableSystemAudioIfExist();
         disableArcIfExist();
+        clearDeviceInfoList();
         checkIfPendingActionsCleared();
     }
 
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 6de8a8b..69f2f32 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -474,6 +474,9 @@
     final SparseArray<PackageVerificationState> mPendingVerification
             = new SparseArray<PackageVerificationState>();
 
+    /** Set of packages associated with each app op permission. */
+    final ArrayMap<String, ArraySet<String>> mAppOpPermissionPackages = new ArrayMap<>();
+
     final PackageInstallerService mInstallerService;
 
     HashSet<PackageParser.Package> mDeferredDexOpt = null;
@@ -2917,6 +2920,17 @@
     }
 
     @Override
+    public String[] getAppOpPermissionPackages(String permissionName) {
+        synchronized (mPackages) {
+            ArraySet<String> pkgs = mAppOpPermissionPackages.get(permissionName);
+            if (pkgs == null) {
+                return null;
+            }
+            return pkgs.toArray(new String[pkgs.size()]);
+        }
+    }
+
+    @Override
     public ResolveInfo resolveIntent(Intent intent, String resolvedType,
             int flags, int userId) {
         if (!sUserManager.exists(userId)) return null;
@@ -6591,6 +6605,31 @@
                     r.append(p.info.name);
                 }
             }
+            if ((p.info.protectionLevel&PermissionInfo.PROTECTION_FLAG_APPOP) != 0) {
+                ArraySet<String> appOpPerms = mAppOpPermissionPackages.get(p.info.name);
+                if (appOpPerms != null) {
+                    appOpPerms.remove(pkg.packageName);
+                }
+            }
+        }
+        if (r != null) {
+            if (DEBUG_REMOVE) Log.d(TAG, "  Permissions: " + r);
+        }
+
+        N = pkg.requestedPermissions.size();
+        r = null;
+        for (i=0; i<N; i++) {
+            String perm = pkg.requestedPermissions.get(i);
+            BasePermission bp = mSettings.mPermissions.get(perm);
+            if (bp != null && (bp.protectionLevel&PermissionInfo.PROTECTION_FLAG_APPOP) != 0) {
+                ArraySet<String> appOpPerms = mAppOpPermissionPackages.get(perm);
+                if (appOpPerms != null) {
+                    appOpPerms.remove(pkg.packageName);
+                    if (appOpPerms.isEmpty()) {
+                        mAppOpPermissionPackages.remove(perm);
+                    }
+                }
+            }
         }
         if (r != null) {
             if (DEBUG_REMOVE) Log.d(TAG, "  Permissions: " + r);
@@ -6775,6 +6814,15 @@
             final String perm = bp.name;
             boolean allowed;
             boolean allowedSig = false;
+            if ((bp.protectionLevel&PermissionInfo.PROTECTION_FLAG_APPOP) != 0) {
+                // Keep track of app op permissions.
+                ArraySet<String> pkgs = mAppOpPermissionPackages.get(bp.name);
+                if (pkgs == null) {
+                    pkgs = new ArraySet<>();
+                    mAppOpPermissionPackages.put(bp.name, pkgs);
+                }
+                pkgs.add(pkg.packageName);
+            }
             final int level = bp.protectionLevel & PermissionInfo.PROTECTION_MASK_BASE;
             if (level == PermissionInfo.PROTECTION_NORMAL
                     || level == PermissionInfo.PROTECTION_DANGEROUS) {
@@ -6837,7 +6885,9 @@
                             + " (protectionLevel=" + bp.protectionLevel
                             + " flags=0x" + Integer.toHexString(pkg.applicationInfo.flags)
                             + ")");
-                } else {
+                } else if ((bp.protectionLevel&PermissionInfo.PROTECTION_FLAG_APPOP) == 0) {
+                    // Don't print warning for app op permissions, since it is fine for them
+                    // not to be granted, there is a UI for the user to decide.
                     Slog.w(TAG, "Not granting permission " + perm
                             + " to package " + pkg.packageName
                             + " (protectionLevel=" + bp.protectionLevel
@@ -12426,6 +12476,22 @@
 
             if (!checkin && dumpState.isDumping(DumpState.DUMP_PERMISSIONS)) {
                 mSettings.dumpPermissionsLPr(pw, packageName, dumpState);
+                if (packageName == null) {
+                    for (int iperm=0; iperm<mAppOpPermissionPackages.size(); iperm++) {
+                        if (iperm == 0) {
+                            if (dumpState.onTitlePrinted())
+                                pw.println();
+                            pw.println("AppOp Permissions:");
+                        }
+                        pw.print("  AppOp Permission ");
+                        pw.print(mAppOpPermissionPackages.keyAt(iperm));
+                        pw.println(":");
+                        ArraySet<String> pkgs = mAppOpPermissionPackages.valueAt(iperm);
+                        for (int ipkg=0; ipkg<pkgs.size(); ipkg++) {
+                            pw.print("    "); pw.println(pkgs.valueAt(ipkg));
+                        }
+                    }
+                }
             }
 
             if (!checkin && dumpState.isDumping(DumpState.DUMP_PROVIDERS)) {
diff --git a/tests/MusicBrowserDemo/Android.mk b/tests/MusicBrowserDemo/Android.mk
new file mode 100644
index 0000000..207774b
--- /dev/null
+++ b/tests/MusicBrowserDemo/Android.mk
@@ -0,0 +1,35 @@
+# 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.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := MusicBrowserDemo
+#LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    android-support-v4 \
+    android-support-v7-appcompat
+
+LOCAL_RESOURCE_DIR := \
+        $(LOCAL_PATH)/res \
+        frameworks/support/v7/appcompat/res
+LOCAL_PROGUARD_ENABLED := disabled
+#LOCAL_PROGUARD_FLAG_FILES := proguard.flags
+
+LOCAL_AAPT_FLAGS := \
+        --auto-add-overlay \
+        --extra-packages android.support.v7.appcompat
+include $(BUILD_PACKAGE)
diff --git a/tests/MusicBrowserDemo/AndroidManifest.xml b/tests/MusicBrowserDemo/AndroidManifest.xml
new file mode 100644
index 0000000..d2acfe2
--- /dev/null
+++ b/tests/MusicBrowserDemo/AndroidManifest.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2014 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.android.musicbrowserdemo"
+    android:versionCode="1"
+    android:versionName="1.0" >
+
+    <uses-sdk
+        android:minSdkVersion="9"
+        android:targetSdkVersion="19" />
+
+    <application
+        android:allowBackup="true"
+        android:icon="@drawable/ic_launcher"
+        android:label="@string/app_name"
+        android:theme="@style/AppTheme"
+        >
+
+        <activity
+            android:name="com.example.android.musicbrowserdemo.MainActivity"
+            android:label="@string/app_name"
+            >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+    </application>
+
+</manifest>
diff --git a/tests/MusicBrowserDemo/res/drawable-hdpi/ic_launcher.png b/tests/MusicBrowserDemo/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..47d6854
--- /dev/null
+++ b/tests/MusicBrowserDemo/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/tests/MusicBrowserDemo/res/drawable-mdpi/ic_launcher.png b/tests/MusicBrowserDemo/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..01b53fd
--- /dev/null
+++ b/tests/MusicBrowserDemo/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/tests/MusicBrowserDemo/res/drawable-xhdpi/ic_launcher.png b/tests/MusicBrowserDemo/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..af762f2
--- /dev/null
+++ b/tests/MusicBrowserDemo/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/tests/MusicBrowserDemo/res/drawable-xxhdpi/ic_launcher.png b/tests/MusicBrowserDemo/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..eef47aa
--- /dev/null
+++ b/tests/MusicBrowserDemo/res/drawable-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/tests/MusicBrowserDemo/res/values/strings.xml b/tests/MusicBrowserDemo/res/values/strings.xml
new file mode 100644
index 0000000..858f278f
--- /dev/null
+++ b/tests/MusicBrowserDemo/res/values/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2014 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<resources>
+
+    <string name="app_name">Music Browser</string>
+
+</resources>
diff --git a/tests/MusicBrowserDemo/res/values/styles.xml b/tests/MusicBrowserDemo/res/values/styles.xml
new file mode 100644
index 0000000..b83662d
--- /dev/null
+++ b/tests/MusicBrowserDemo/res/values/styles.xml
@@ -0,0 +1,36 @@
+<?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>
+
+    <!--
+        Base application theme, dependent on API level. This theme is replaced
+        by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
+    -->
+    <style name="AppBaseTheme" parent="Theme.AppCompat.Light">
+        <!--
+            Theme customizations available in newer API levels can go in
+            res/values-vXX/styles.xml, while customizations related to
+            backward-compatibility can go here.
+        -->
+    </style>
+
+    <!-- Application theme. -->
+    <style name="AppTheme" parent="AppBaseTheme">
+        <!-- All customizations that are NOT specific to a particular API-level can go here. -->
+    </style>
+
+</resources>
diff --git a/tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/AppListFragment.java b/tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/AppListFragment.java
new file mode 100644
index 0000000..c0f3a7f
--- /dev/null
+++ b/tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/AppListFragment.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2010 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.musicbrowserdemo;
+
+import android.content.Context;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.media.browse.MediaBrowserService;
+import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.app.ListFragment;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+// TODO: Include an icon.
+
+public class AppListFragment extends ListFragment {
+
+    private Adapter mAdapter;
+    private List<Item> mItems;
+
+    public AppListFragment() {
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        mAdapter = new Adapter();
+        setListAdapter(mAdapter);
+    }
+
+    @Override
+    public void onListItemClick(ListView l, View v, int position, long id) {
+        final Item item = mItems.get(position);
+
+        Log.i("AppListFragment", "Item clicked: " + position + " -- " + item.component);
+
+        final BrowserListFragment fragment = new BrowserListFragment();
+
+        final Bundle args = new Bundle();
+        args.putParcelable(BrowserListFragment.ARG_COMPONENT, item.component);
+        fragment.setArguments(args);
+
+        getFragmentManager().beginTransaction()
+                .replace(android.R.id.content, fragment)
+                .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
+                .addToBackStack(null)
+                .commit();
+    }
+
+    private static class Item {
+        final String label;
+        final ComponentName component;
+
+        Item(String l, ComponentName c) {
+            this.label = l;
+            this.component = c;
+        }
+    }
+
+    private class Adapter extends BaseAdapter {
+        private final LayoutInflater mInflater;
+
+        Adapter() {
+            super();
+
+            final Context context = getActivity();
+            mInflater = LayoutInflater.from(context);
+
+            // Load the data
+            final PackageManager pm = context.getPackageManager();
+            final Intent intent = new Intent(MediaBrowserService.SERVICE_ACTION);
+            final List<ResolveInfo> list = pm.queryIntentServices(intent, 0);
+            final int N = list.size();
+            mItems = new ArrayList(N);
+            for (int i=0; i<N; i++) {
+                final ResolveInfo ri = list.get(i);
+                mItems.add(new Item(ri.loadLabel(pm).toString(), new ComponentName(
+                            ri.serviceInfo.applicationInfo.packageName,
+                            ri.serviceInfo.name)));
+            }
+        }
+
+        @Override
+        public int getCount() {
+            return mItems.size();
+        }
+
+        @Override
+        public Item getItem(int position) {
+            return mItems.get(position);
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            return 1;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            if (convertView == null) {
+                convertView = mInflater.inflate(android.R.layout.simple_list_item_1, parent, false);
+            }
+
+            final TextView tv = (TextView)convertView;
+            final Item item = mItems.get(position);
+            tv.setText(item.label);
+
+            return convertView;
+        }
+
+        @Override
+        public int getViewTypeCount() {
+            return 1;
+        }
+    }
+}
+
+
diff --git a/tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/BrowserListFragment.java b/tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/BrowserListFragment.java
new file mode 100644
index 0000000..3fc468d
--- /dev/null
+++ b/tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/BrowserListFragment.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2010 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.musicbrowserdemo;
+
+import android.content.Context;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.media.browse.MediaBrowser;
+import android.media.browse.MediaBrowserItem;
+import android.media.browse.MediaBrowserService;
+import android.os.Bundle;
+import android.net.Uri;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.app.ListFragment;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class BrowserListFragment extends ListFragment {
+    private static final String TAG = "BrowserListFragment";
+
+    // Hints
+    public static final String HINT_DISPLAY = "com.example.android.musicbrowserdemo.DISPLAY";
+
+    // For args
+    public static final String ARG_COMPONENT = "component";
+    public static final String ARG_URI = "uri";
+
+    private Adapter mAdapter;
+    private List<Item> mItems = new ArrayList();
+    private ComponentName mComponent;
+    private Uri mUri;
+    private MediaBrowser mBrowser;
+
+    private static class Item {
+        final MediaBrowserItem media;
+
+        Item(MediaBrowserItem m) {
+            this.media = m;
+        }
+    }
+
+    public BrowserListFragment() {
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        Log.d(TAG, "onActivityCreated -- " + hashCode());
+        mAdapter = new Adapter();
+        setListAdapter(mAdapter);
+
+        // Get our arguments
+        final Bundle args = getArguments();
+        mComponent = args.getParcelable(ARG_COMPONENT);
+        mUri = args.getParcelable(ARG_URI);
+
+        // A hint about who we are, so the service can customize the results if it wants to.
+        final Bundle rootHints = new Bundle();
+        rootHints.putBoolean(HINT_DISPLAY, true);
+
+        mBrowser = new MediaBrowser(getActivity(), mComponent, mConnectionCallbacks, rootHints);
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        mBrowser.connect();
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        mBrowser.disconnect();
+    }
+
+    @Override
+    public void onListItemClick(ListView l, View v, int position, long id) {
+        final Item item = mItems.get(position);
+
+        Log.i("BrowserListFragment", "Item clicked: " + position + " -- "
+                + mAdapter.getItem(position).media.getUri());
+
+        final BrowserListFragment fragment = new BrowserListFragment();
+
+        final Bundle args = new Bundle();
+        args.putParcelable(BrowserListFragment.ARG_COMPONENT, mComponent);
+        args.putParcelable(BrowserListFragment.ARG_URI, item.media.getUri());
+        fragment.setArguments(args);
+
+        getFragmentManager().beginTransaction()
+                .replace(android.R.id.content, fragment)
+                .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
+                .addToBackStack(null)
+                .commit();
+
+    }
+
+    final MediaBrowser.ConnectionCallback mConnectionCallbacks
+            = new MediaBrowser.ConnectionCallback() {
+        @Override
+        public void onConnected() {
+            Log.d(TAG, "mConnectionCallbacks.onConnected");
+            if (mUri == null) {
+                mUri = mBrowser.getRoot();
+            }
+            mBrowser.subscribe(mUri, new MediaBrowser.SubscriptionCallback() {
+                    @Override
+                    public void onChildrenLoaded(Uri parentUri, List<MediaBrowserItem> children) {
+                        Log.d(TAG, "onChildrenLoaded parentUri=" + parentUri
+                                + " children= " + children);
+                        mItems.clear();
+                        final int N = children.size();
+                        for (int i=0; i<N; i++) {
+                            mItems.add(new Item(children.get(i)));
+                        }
+                        mAdapter.notifyDataSetChanged();
+                    }
+
+                    @Override
+                    public void onError(Uri parentUri) {
+                        Log.d(TAG, "onError parentUri=" + parentUri);
+                    }
+                });
+        }
+
+        @Override
+        public void onConnectionSuspended() {
+            Log.d(TAG, "mConnectionCallbacks.onConnectionSuspended");
+        }
+
+        @Override
+        public void onConnectionFailed() {
+            Log.d(TAG, "mConnectionCallbacks.onConnectionFailed");
+        }
+    };
+
+    private class Adapter extends BaseAdapter {
+        private final LayoutInflater mInflater;
+
+        Adapter() {
+            super();
+
+            final Context context = getActivity();
+            mInflater = LayoutInflater.from(context);
+        }
+
+        @Override
+        public int getCount() {
+            return mItems.size();
+        }
+
+        @Override
+        public Item getItem(int position) {
+            return mItems.get(position);
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            return 1;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            if (convertView == null) {
+                convertView = mInflater.inflate(android.R.layout.simple_list_item_1, parent, false);
+            }
+
+            final TextView tv = (TextView)convertView;
+            final Item item = mItems.get(position);
+            tv.setText(item.media.getTitle());
+
+            return convertView;
+        }
+
+        @Override
+        public int getViewTypeCount() {
+            return 1;
+        }
+    }
+}
+
+
diff --git a/tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/MainActivity.java b/tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/MainActivity.java
new file mode 100644
index 0000000..ed91aad
--- /dev/null
+++ b/tests/MusicBrowserDemo/src/com/example/android/musicbrowserdemo/MainActivity.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010 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.musicbrowserdemo;
+
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+/**
+ * Main activity class.
+ */
+public class MainActivity extends FragmentActivity {
+
+    private static final String BROWSER_FRAGMENT_TAG = "browser";
+
+    @Override
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+
+        Log.d("MainActivity", "-------------------------------------------------------");
+
+        // If we are starting afresh, start at the app list.
+        final FragmentManager fm = getSupportFragmentManager();
+        if (fm.findFragmentById(android.R.id.content) == null) {
+            fm.beginTransaction().add(android.R.id.content, new AppListFragment()).commit();
+        }
+    }
+}
+
diff --git a/tests/MusicServiceDemo/Android.mk b/tests/MusicServiceDemo/Android.mk
new file mode 100644
index 0000000..feef67a
--- /dev/null
+++ b/tests/MusicServiceDemo/Android.mk
@@ -0,0 +1,35 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := MusicServiceDemo
+#LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    android-support-v4 \
+    android-support-v7-appcompat
+
+LOCAL_RESOURCE_DIR := \
+        $(LOCAL_PATH)/res \
+        frameworks/support/v7/appcompat/res
+LOCAL_PROGUARD_ENABLED := disabled
+#LOCAL_PROGUARD_FLAG_FILES := proguard.flags
+
+LOCAL_AAPT_FLAGS := \
+        --auto-add-overlay \
+        --extra-packages android.support.v7.appcompat
+include $(BUILD_PACKAGE)
diff --git a/tests/MusicServiceDemo/AndroidManifest.xml b/tests/MusicServiceDemo/AndroidManifest.xml
new file mode 100644
index 0000000..4178a80
--- /dev/null
+++ b/tests/MusicServiceDemo/AndroidManifest.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2014 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.android.musicservicedemo"
+    android:versionCode="1"
+    android:versionName="1.0" >
+
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+
+    <uses-sdk
+        android:minSdkVersion="9"
+        android:targetSdkVersion="19" />
+
+    <application
+        android:allowBackup="true"
+        android:icon="@drawable/ic_launcher"
+        android:label="@string/app_name"
+        android:theme="@style/AppTheme"
+        >
+
+        <activity
+            android:name="com.example.android.automotive.musicplayer.MainActivity"
+            android:label="@string/app_name" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+        <service
+            android:name=".BrowserService"
+            android:exported="true"
+            >
+            <intent-filter>
+                <action android:name="android.media.browse.MediaBrowseService" />
+            </intent-filter>
+        </service>
+    </application>
+
+</manifest>
diff --git a/tests/MusicServiceDemo/proguard-project.txt b/tests/MusicServiceDemo/proguard-project.txt
new file mode 100644
index 0000000..f2fe155
--- /dev/null
+++ b/tests/MusicServiceDemo/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
diff --git a/tests/MusicServiceDemo/res/drawable-hdpi/ic_launcher.png b/tests/MusicServiceDemo/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..47d6854
--- /dev/null
+++ b/tests/MusicServiceDemo/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/tests/MusicServiceDemo/res/drawable-mdpi/ic_launcher.png b/tests/MusicServiceDemo/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..01b53fd
--- /dev/null
+++ b/tests/MusicServiceDemo/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/tests/MusicServiceDemo/res/drawable-xhdpi/ic_launcher.png b/tests/MusicServiceDemo/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..af762f2
--- /dev/null
+++ b/tests/MusicServiceDemo/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/tests/MusicServiceDemo/res/drawable-xxhdpi/ic_launcher.png b/tests/MusicServiceDemo/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..eef47aa
--- /dev/null
+++ b/tests/MusicServiceDemo/res/drawable-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/tests/MusicServiceDemo/res/drawable-xxhdpi/thumbsup.png b/tests/MusicServiceDemo/res/drawable-xxhdpi/thumbsup.png
new file mode 100644
index 0000000..ea98c95
--- /dev/null
+++ b/tests/MusicServiceDemo/res/drawable-xxhdpi/thumbsup.png
Binary files differ
diff --git a/tests/MusicServiceDemo/res/layout/activity_main.xml b/tests/MusicServiceDemo/res/layout/activity_main.xml
new file mode 100644
index 0000000..71753e3
--- /dev/null
+++ b/tests/MusicServiceDemo/res/layout/activity_main.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2014 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context="com.example.android.automotive.musicplayer.MainActivity"
+    tools:ignore="MergeRootFrame" />
diff --git a/tests/MusicServiceDemo/res/layout/fragment_main.xml b/tests/MusicServiceDemo/res/layout/fragment_main.xml
new file mode 100644
index 0000000..8796e86
--- /dev/null
+++ b/tests/MusicServiceDemo/res/layout/fragment_main.xml
@@ -0,0 +1,32 @@
+<?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.
+  -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:paddingBottom="@dimen/activity_vertical_margin"
+    android:paddingLeft="@dimen/activity_horizontal_margin"
+    android:paddingRight="@dimen/activity_horizontal_margin"
+    android:paddingTop="@dimen/activity_vertical_margin"
+    tools:context="com.example.android.automotive.musicplayer.MainActivity$PlaceholderFragment" >
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/app_name" />
+
+</RelativeLayout>
diff --git a/tests/MusicServiceDemo/res/values/colors.xml b/tests/MusicServiceDemo/res/values/colors.xml
new file mode 100644
index 0000000..44dd05d
--- /dev/null
+++ b/tests/MusicServiceDemo/res/values/colors.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <color name="yellow">#ffffff00</color>
+    <color name="green">#ff00ff00</color>
+    <color name="blue">#ff0000ff</color>
+    <color name="red">#ffff0000</color>
+</resources>
diff --git a/tests/MusicServiceDemo/res/values/dimens.xml b/tests/MusicServiceDemo/res/values/dimens.xml
new file mode 100644
index 0000000..9f63ef2
--- /dev/null
+++ b/tests/MusicServiceDemo/res/values/dimens.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2014 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<resources>
+
+    <!-- Default screen margins, per the Android Design guidelines. -->
+    <dimen name="activity_horizontal_margin">16dp</dimen>
+    <dimen name="activity_vertical_margin">16dp</dimen>
+
+</resources>
diff --git a/tests/MusicServiceDemo/res/values/strings.xml b/tests/MusicServiceDemo/res/values/strings.xml
new file mode 100644
index 0000000..14c0171
--- /dev/null
+++ b/tests/MusicServiceDemo/res/values/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2014 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<resources>
+
+    <string name="app_name">Music Service Demo</string>
+    <string name="action_settings">Settings</string>
+    <string name="thumbs_up">Thumbs Up</string>
+    <string name="music_error">No music found</string>
+    <string name="now_playing">Now Playing</string>
+
+</resources>
diff --git a/tests/MusicServiceDemo/res/values/styles.xml b/tests/MusicServiceDemo/res/values/styles.xml
new file mode 100644
index 0000000..b83662d
--- /dev/null
+++ b/tests/MusicServiceDemo/res/values/styles.xml
@@ -0,0 +1,36 @@
+<?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>
+
+    <!--
+        Base application theme, dependent on API level. This theme is replaced
+        by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
+    -->
+    <style name="AppBaseTheme" parent="Theme.AppCompat.Light">
+        <!--
+            Theme customizations available in newer API levels can go in
+            res/values-vXX/styles.xml, while customizations related to
+            backward-compatibility can go here.
+        -->
+    </style>
+
+    <!-- Application theme. -->
+    <style name="AppTheme" parent="AppBaseTheme">
+        <!-- All customizations that are NOT specific to a particular API-level can go here. -->
+    </style>
+
+</resources>
diff --git a/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/BrowserService.java b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/BrowserService.java
new file mode 100644
index 0000000..9ca156f
--- /dev/null
+++ b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/BrowserService.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2014 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicservicedemo;
+
+import android.app.SearchManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.UriMatcher;
+import android.content.res.Resources.NotFoundException;
+import android.database.MatrixCursor;
+import android.graphics.Bitmap;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.media.MediaPlayer.OnCompletionListener;
+import android.media.MediaPlayer.OnErrorListener;
+import android.media.MediaPlayer.OnPreparedListener;
+import android.media.browse.MediaBrowserItem;
+import android.media.browse.MediaBrowserService;
+import android.media.session.MediaSession;
+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.IBinder;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.Log;
+
+import com.example.android.musicservicedemo.browser.MusicProvider;
+import com.example.android.musicservicedemo.browser.MusicProviderTask;
+import com.example.android.musicservicedemo.browser.MusicProviderTaskListener;
+import com.example.android.musicservicedemo.browser.MusicTrack;
+
+import org.json.JSONException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Service that implements MediaBrowserService and returns our menu hierarchy.
+ */
+public class BrowserService extends MediaBrowserService {
+    private static final String TAG = "BrowserService";
+
+    // URI paths for browsing music
+    public static final String BROWSE_ROOT_BASE_PATH = "browse";
+    public static final String NOW_PLAYING_PATH = "now_playing";
+    public static final String PIANO_BASE_PATH = "piano";
+    public static final String VOICE_BASE_PATH = "voice";
+
+    // Content URIs
+    public static final String AUTHORITY = "com.example.android.automotive.musicplayer";
+    public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
+    public static final Uri BROWSE_URI = Uri.withAppendedPath(BASE_URI, BROWSE_ROOT_BASE_PATH);
+
+    // URI matcher constants for browsing paths
+    public static final int BROWSE_ROOT = 1;
+    public static final int NOW_PLAYING = 2;
+    public static final int PIANO = 3;
+    public static final int VOICE = 4;
+
+    // Map the the URI paths with the URI matcher constants
+    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+    static {
+        sUriMatcher.addURI(AUTHORITY, BROWSE_ROOT_BASE_PATH, BROWSE_ROOT);
+        sUriMatcher.addURI(AUTHORITY, NOW_PLAYING_PATH, NOW_PLAYING);
+        sUriMatcher.addURI(AUTHORITY, PIANO_BASE_PATH, PIANO);
+        sUriMatcher.addURI(AUTHORITY, VOICE_BASE_PATH, VOICE);
+    }
+
+    // Media metadata that will be provided for a media container
+    public static final String[] MEDIA_CONTAINER_PROJECTION = {
+            "uri",
+            "title",
+            "subtitle",
+            "image_uri",
+            "supported_actions"
+    };
+
+    // MusicProvider will download the music catalog
+    private MusicProvider mMusicProvider;
+
+    private MediaSession mSession;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+
+        mSession = new MediaSession(this, "com.example.android.musicservicedemo.BrowserService");
+        setSessionToken(mSession.getSessionToken());
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+    }
+
+    @Override
+    public Uri onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
+        return BROWSE_URI;
+    }
+
+    @Override
+    public List<MediaBrowserItem> onLoadChildren(Uri parentUri) {
+        final ArrayList<MediaBrowserItem> results = new ArrayList();
+
+        for (int i=0; i<10; i++) {
+            results.add(new MediaBrowserItem.Builder(Uri.withAppendedPath(BASE_URI, Integer.toString(i)),
+                    MediaBrowserItem.FLAG_BROWSABLE, "Title " + i).setSummary("Summary " + i).build());
+        }
+
+        return results;
+    }
+
+    @Override
+    public Bitmap onGetThumbnail(Uri uri, int width, int height, int density) {
+        return null;
+    }
+
+    /*
+    @Override
+    public void query(final Query query, final IMetadataResultHandler metadataResultHandler,
+            final IErrorHandler errorHandler)
+            throws RemoteException {
+        Log.d(TAG, "query: " + query);
+        Utils.checkNotNull(query);
+        Utils.checkNotNull(metadataResultHandler);
+        Utils.checkNotNull(errorHandler);
+
+        // Handle async response
+        new Thread(new Runnable() {
+            public void run() {
+                try {
+                    // Pre-load the list of music
+                    List<MusicTrack> musicTracks = getMusicList();
+                    if (musicTracks == null) {
+                        notifyListenersOnPlaybackStateUpdate(getCurrentPlaybackState());
+                        errorHandler.onError(new Error(Error.UNKNOWN,
+                                getString(R.string.music_error)));
+                        return;
+                    }
+
+                    final Uri uri = query.getUri();
+                    int match = sUriMatcher.match(uri);
+                    Log.d(TAG, "Queried: " + uri + "; match: " + match);
+                    switch (match) {
+                        case BROWSE_ROOT:
+                        {
+                            Log.d(TAG, "Browse_root");
+
+                            try {
+                                MatrixCursor matrixCursor = mMusicProvider
+                                        .getRootContainerCurser();
+                                DataHolder holder = new DataHolder(MEDIA_CONTAINER_PROJECTION,
+                                        matrixCursor, null);
+
+                                Log.d(TAG, "on metadata response called " + holder.getCount());
+                                metadataResultHandler.onMetadataResponse(holder);
+                            } catch (RemoteException e) {
+                                Log.w(TAG, "Error delivering metadata in the callback.", e);
+                            }
+                            break;
+                        }
+                        case NOW_PLAYING:
+                        {
+                            try {
+                                Log.d(TAG, "query NOW_PLAYING");
+                                MatrixCursor matrixCursor = mMusicProvider
+                                        .getRootItemCursor(
+                                        PIANO);
+                                DataHolder holder = new DataHolder(MEDIA_CONTAINER_PROJECTION,
+                                        matrixCursor, null);
+                                Log.d(TAG, "on metadata response called " + holder.getCount());
+                                metadataResultHandler.onMetadataResponse(holder);
+                            } catch (RemoteException e) {
+                                Log.w(TAG, "Error querying NOW_PLAYING");
+                            }
+                            break;
+                        }
+                        case PIANO:
+                        {
+                            try {
+                                Log.d(TAG, "query PIANO");
+                                MatrixCursor matrixCursor = mMusicProvider
+                                        .getRootItemCursor(
+                                        PIANO);
+                                DataHolder holder = new DataHolder(MEDIA_CONTAINER_PROJECTION,
+                                        matrixCursor, null);
+                                Log.d(TAG, "on metadata response called " + holder.getCount());
+                                metadataResultHandler.onMetadataResponse(holder);
+                            } catch (RemoteException e) {
+                                Log.w(TAG, "Error querying PIANO");
+                            }
+                            break;
+                        }
+                        case VOICE:
+                        {
+                            try {
+                                Log.d(TAG, "query VOICE");
+                                MatrixCursor matrixCursor = mMusicProvider
+                                        .getRootItemCursor(
+                                        VOICE);
+                                DataHolder holder = new DataHolder(MEDIA_CONTAINER_PROJECTION,
+                                        matrixCursor, null);
+                                Log.d(TAG, "on metadata response called " + holder.getCount());
+                                metadataResultHandler.onMetadataResponse(holder);
+                            } catch (RemoteException e) {
+                                Log.w(TAG, "Error querying VOICE");
+                            }
+                            break;
+                        }
+                        default:
+                        {
+                            Log.w(TAG, "Skipping unmatched URI: " + uri);
+                        }
+                    }
+                } catch (NotFoundException e) {
+                    Log.e(TAG, "::run:", e);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "::run:", e);
+                }
+            } // end run
+        }).start();
+    }
+
+    */
+}
diff --git a/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/MainActivity.java b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/MainActivity.java
new file mode 100644
index 0000000..db45b9d
--- /dev/null
+++ b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/MainActivity.java
@@ -0,0 +1,101 @@
+/* Copyright 2014 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicservicedemo;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v7.app.ActionBarActivity;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.example.android.musicservicedemo.R;
+
+// TODO Local UI
+
+/**
+ * Main activity of the app.
+ */
+public class MainActivity extends ActionBarActivity {
+
+    private static final String LOG = "MainActivity";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+
+        if (savedInstanceState == null) {
+            getSupportFragmentManager().beginTransaction()
+                    .add(R.id.container, new PlaceholderFragment())
+                    .commit();
+        }
+
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see android.app.Activity#onCreateOptionsMenu(android.view.Menu)
+     */
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+
+        // Inflate the menu; this adds items to the action bar if it is present.
+        //getMenuInflater().inflate(R.menu.main, menu);
+        return true;
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see android.app.Activity#onOptionsItemSelected(android.view.MenuItem)
+     */
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        // Handle action bar item clicks here. The action bar will
+        // automatically handle clicks on the Home/Up button, so long
+        // as you specify a parent activity in AndroidManifest.xml.
+        int id = item.getItemId();
+        // if (id == R.id.action_settings) {
+        // return true;
+        // }
+        return super.onOptionsItemSelected(item);
+    }
+
+    /**
+     * A placeholder fragment containing a simple view.
+     */
+    public static class PlaceholderFragment extends Fragment {
+
+        public PlaceholderFragment() {
+        }
+
+        /*
+         * (non-Javadoc)
+         * @see
+         * android.support.v4.app.Fragment#onCreateView(android.view.LayoutInflater
+         * , android.view.ViewGroup, android.os.Bundle)
+         */
+        @Override
+        public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                Bundle savedInstanceState) {
+            View rootView = inflater.inflate(R.layout.fragment_main, container, false);
+            return rootView;
+        }
+    }
+
+}
diff --git a/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/Utils.java b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/Utils.java
new file mode 100644
index 0000000..3589761
--- /dev/null
+++ b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/Utils.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2014 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicservicedemo;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+public class Utils {
+
+    private static final String TAG = "Utils";
+
+    /**
+     * Utility method to check that parameters are not null
+     *
+     * @param object
+     */
+    public static final void checkNotNull(Object object) {
+        if (object == null) {
+            throw new NullPointerException();
+        }
+    }
+
+    /**
+     * Utility to download a bitmap
+     *
+     * @param source
+     * @return
+     */
+    public static Bitmap getBitmapFromURL(String source) {
+        try {
+            URL url = new URL(source);
+            HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
+            httpConnection.setDoInput(true);
+            httpConnection.connect();
+            InputStream inputStream = httpConnection.getInputStream();
+            return BitmapFactory.decodeStream(inputStream);
+        } catch (IOException e) {
+            Log.e(TAG, "getBitmapFromUrl: " + source, e);
+        }
+        return null;
+    }
+
+    /**
+     * Utility method to wrap an index
+     *
+     * @param i
+     * @param size
+     * @return
+     */
+    public static int wrapIndex(int i, int size) {
+        int m = i % size;
+        if (m < 0) { // java modulus can be negative
+            m += size;
+        }
+        return m;
+    }
+}
diff --git a/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProvider.java b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProvider.java
new file mode 100644
index 0000000..15038d7
--- /dev/null
+++ b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProvider.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2014 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicservicedemo.browser;
+
+import android.database.MatrixCursor;
+import android.media.session.PlaybackState;
+import android.net.Uri;
+import android.util.Log;
+
+import com.example.android.musicservicedemo.BrowserService;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility class to get a list of MusicTrack's based on a server-side JSON
+ * configuration.
+ */
+public class MusicProvider {
+
+    private static final String TAG = "MusicProvider";
+
+    private static final String MUSIC_URL = "http://storage.googleapis.com/automotive-media/music.json";
+
+    private static String MUSIC = "music";
+    private static String TITLE = "title";
+    private static String ALBUM = "album";
+    private static String ARTIST = "artist";
+    private static String GENRE = "genre";
+    private static String SOURCE = "source";
+    private static String IMAGE = "image";
+    private static String TRACK_NUMBER = "trackNumber";
+    private static String TOTAL_TRACK_COUNT = "totalTrackCount";
+    private static String DURATION = "duration";
+
+    // Cache for music track data
+    private static List<MusicTrack> mMusicList;
+
+    /**
+     * Get the cached list of music tracks
+     *
+     * @return
+     * @throws JSONException
+     */
+    public List<MusicTrack> getMedia() throws JSONException {
+        if (null != mMusicList && mMusicList.size() > 0) {
+            return mMusicList;
+        }
+        return null;
+    }
+
+    /**
+     * Get the list of music tracks from a server and return the list of
+     * MusicTrack objects.
+     *
+     * @return
+     * @throws JSONException
+     */
+    public List<MusicTrack> retreiveMedia() throws JSONException {
+        if (null != mMusicList) {
+            return mMusicList;
+        }
+        int slashPos = MUSIC_URL.lastIndexOf('/');
+        String path = MUSIC_URL.substring(0, slashPos + 1);
+        JSONObject jsonObj = parseUrl(MUSIC_URL);
+
+        try {
+            JSONArray videos = jsonObj.getJSONArray(MUSIC);
+            if (null != videos) {
+                mMusicList = new ArrayList<MusicTrack>();
+                for (int j = 0; j < videos.length(); j++) {
+                    JSONObject music = videos.getJSONObject(j);
+                    String title = music.getString(TITLE);
+                    String album = music.getString(ALBUM);
+                    String artist = music.getString(ARTIST);
+                    String genre = music.getString(GENRE);
+                    String source = music.getString(SOURCE);
+                    // Media is stored relative to JSON file
+                    if (!source.startsWith("http")) {
+                        source = path + source;
+                    }
+                    String image = music.getString(IMAGE);
+                    if (!image.startsWith("http")) {
+                        image = path + image;
+                    }
+                    int trackNumber = music.getInt(TRACK_NUMBER);
+                    int totalTrackCount = music.getInt(TOTAL_TRACK_COUNT);
+                    int duration = music.getInt(DURATION) * 1000; // ms
+
+                    mMusicList.add(new MusicTrack(title, album, artist, genre, source,
+                            image, trackNumber, totalTrackCount, duration));
+                }
+            }
+        } catch (NullPointerException e) {
+            Log.e(TAG, "retreiveMedia", e);
+        }
+        return mMusicList;
+    }
+
+    /**
+     * Download a JSON file from a server, parse the content and return the JSON
+     * object.
+     *
+     * @param urlString
+     * @return
+     */
+    private JSONObject parseUrl(String urlString) {
+        InputStream is = null;
+        try {
+            java.net.URL url = new java.net.URL(urlString);
+            URLConnection urlConnection = url.openConnection();
+            is = new BufferedInputStream(urlConnection.getInputStream());
+            BufferedReader reader = new BufferedReader(new InputStreamReader(
+                    urlConnection.getInputStream(), "iso-8859-1"), 8);
+            StringBuilder sb = new StringBuilder();
+            String line = null;
+            while ((line = reader.readLine()) != null) {
+                sb.append(line);
+            }
+            return new JSONObject(sb.toString());
+        } catch (Exception e) {
+            Log.d(TAG, "Failed to parse the json for media list", e);
+            return null;
+        } finally {
+            if (null != is) {
+                try {
+                    is.close();
+                } catch (IOException e) {
+                    // ignore
+                }
+            }
+        }
+    }
+
+    public MatrixCursor getRootContainerCurser() {
+        MatrixCursor matrixCursor = new MatrixCursor(BrowserService.MEDIA_CONTAINER_PROJECTION);
+        Uri.Builder pianoBuilder = new Uri.Builder();
+        pianoBuilder.authority(BrowserService.AUTHORITY);
+        pianoBuilder.appendPath(BrowserService.PIANO_BASE_PATH);
+        matrixCursor.addRow(new Object[] {
+                pianoBuilder.build(),
+                BrowserService.PIANO_BASE_PATH,
+                "subtitle",
+                null,
+                0
+        });
+
+        Uri.Builder voiceBuilder = new Uri.Builder();
+        voiceBuilder.authority(BrowserService.AUTHORITY);
+        voiceBuilder.appendPath(BrowserService.VOICE_BASE_PATH);
+        matrixCursor.addRow(new Object[] {
+                voiceBuilder.build(),
+                BrowserService.VOICE_BASE_PATH,
+                "subtitle",
+                null,
+                0
+        });
+        return matrixCursor;
+    }
+
+    public MatrixCursor getRootItemCursor(int type) {
+        if (type == BrowserService.NOW_PLAYING) {
+            MatrixCursor matrixCursor = new MatrixCursor(BrowserService.MEDIA_CONTAINER_PROJECTION);
+
+            try {
+                // Just return all of the tracks for now
+                List<MusicTrack> musicTracks = retreiveMedia();
+                for (MusicTrack musicTrack : musicTracks) {
+                    Uri.Builder builder = new Uri.Builder();
+                    builder.authority(BrowserService.AUTHORITY);
+                    builder.appendPath(BrowserService.NOW_PLAYING_PATH);
+                    builder.appendPath(musicTrack.getTitle());
+                    matrixCursor.addRow(new Object[] {
+                            builder.build(),
+                            musicTrack.getTitle(),
+                            musicTrack.getArtist(),
+                            musicTrack.getImage(),
+                            PlaybackState.ACTION_PLAY
+                    });
+                    Log.d(TAG, "Uri " + builder.build());
+                }
+            } catch (JSONException e) {
+                Log.e(TAG, "::getRootItemCursor:", e);
+            }
+
+            Log.d(TAG, "cursor: " + matrixCursor.getCount());
+            return matrixCursor;
+        } else if (type == BrowserService.PIANO) {
+            MatrixCursor matrixCursor = new MatrixCursor(BrowserService.MEDIA_CONTAINER_PROJECTION);
+
+            try {
+                List<MusicTrack> musicTracks = retreiveMedia();
+                for (MusicTrack musicTrack : musicTracks) {
+                    Uri.Builder builder = new Uri.Builder();
+                    builder.authority(BrowserService.AUTHORITY);
+                    builder.appendPath(BrowserService.PIANO_BASE_PATH);
+                    builder.appendPath(musicTrack.getTitle());
+                    matrixCursor.addRow(new Object[] {
+                            builder.build(),
+                            musicTrack.getTitle(),
+                            musicTrack.getArtist(),
+                            musicTrack.getImage(),
+                            PlaybackState.ACTION_PLAY
+                    });
+                    Log.d(TAG, "Uri " + builder.build());
+                }
+            } catch (JSONException e) {
+                Log.e(TAG, "::getRootItemCursor:", e);
+            }
+
+            Log.d(TAG, "cursor: " + matrixCursor.getCount());
+            return matrixCursor;
+        } else if (type == BrowserService.VOICE) {
+            MatrixCursor matrixCursor = new MatrixCursor(BrowserService.MEDIA_CONTAINER_PROJECTION);
+
+            try {
+                List<MusicTrack> musicTracks = retreiveMedia();
+                for (MusicTrack musicTrack : musicTracks) {
+                    Uri.Builder builder = new Uri.Builder();
+                    builder.authority(BrowserService.AUTHORITY);
+                    builder.appendPath(BrowserService.VOICE_BASE_PATH);
+                    builder.appendPath(musicTrack.getTitle());
+                    matrixCursor.addRow(new Object[] {
+                            builder.build(),
+                            musicTrack.getTitle(),
+                            musicTrack.getArtist(),
+                            musicTrack.getImage(),
+                            PlaybackState.ACTION_PLAY
+                    });
+                    Log.d(TAG, "Uri " + builder.build());
+                }
+            } catch (JSONException e) {
+                Log.e(TAG, "::getRootItemCursor:", e);
+            }
+
+            Log.d(TAG, "cursor: " + matrixCursor.getCount());
+            return matrixCursor;
+
+        }
+        return null;
+    }
+}
diff --git a/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProviderTask.java b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProviderTask.java
new file mode 100644
index 0000000..ffda110
--- /dev/null
+++ b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProviderTask.java
@@ -0,0 +1,70 @@
+/*  
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicservicedemo.browser;
+
+import android.os.AsyncTask;
+import android.util.Log;
+
+import org.json.JSONException;
+
+/**
+ * Asynchronous task to retrieve the music data using MusicProvider.
+ */
+public class MusicProviderTask extends AsyncTask<Void, Void, Void> {
+
+    private static final String TAG = "MusicProviderTask";
+
+    MusicProvider mMusicProvider;
+    MusicProviderTaskListener mMusicProviderTaskListener;
+
+    /**
+     * Initialize the task with the provider to download the music data and the
+     * listener to be informed when the task is done.
+     *
+     * @param musicProvider
+     * @param listener
+     */
+    public MusicProviderTask(MusicProvider musicProvider,
+            MusicProviderTaskListener listener) {
+        mMusicProvider = musicProvider;
+        mMusicProviderTaskListener = listener;
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see android.os.AsyncTask#doInBackground(java.lang.Object[])
+     */
+    @Override
+    protected Void doInBackground(Void... arg0) {
+        try {
+            mMusicProvider.retreiveMedia();
+        } catch (JSONException e) {
+            Log.e(TAG, "::doInBackground:", e);
+        }
+        return null;
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see android.os.AsyncTask#onPostExecute(java.lang.Object)
+     */
+    @Override
+    protected void onPostExecute(Void result) {
+        mMusicProviderTaskListener.onMusicProviderTaskCompleted();
+    }
+
+}
diff --git a/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProviderTaskListener.java b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProviderTaskListener.java
new file mode 100644
index 0000000..b1d168f
--- /dev/null
+++ b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicProviderTaskListener.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2014 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicservicedemo.browser;
+
+/**
+ * Callback listener for completion of MusicProviderTask
+ */
+public interface MusicProviderTaskListener {
+    public void onMusicProviderTaskCompleted();
+}
diff --git a/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicTrack.java b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicTrack.java
new file mode 100644
index 0000000..02ea899
--- /dev/null
+++ b/tests/MusicServiceDemo/src/com/example/android/musicservicedemo/browser/MusicTrack.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2014 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.musicservicedemo.browser;
+
+/**
+ * A class to model music track metadata.
+ */
+public class MusicTrack {
+
+    private static final String TAG = "MusicTrack";
+
+    private String mTitle;
+    private String mAlbum;
+    private String mArtist;
+    private String mGenre;
+    private String mSource;
+    private String mImage;
+    private int mTrackNumber;
+    private int mTotalTrackCount;
+    private int mDuration;
+
+    /**
+     * Constructor creating a MusicTrack instance.
+     *
+     * @param title
+     * @param album
+     * @param artist
+     * @param genre
+     * @param source
+     * @param image
+     * @param trackNumber
+     * @param totalTrackCount
+     * @param duration
+     */
+    public MusicTrack(String title, String album, String artist, String genre, String source,
+            String image, int trackNumber, int totalTrackCount, int duration) {
+        this.mTitle = title;
+        this.mAlbum = album;
+        this.mArtist = artist;
+        this.mGenre = genre;
+        this.mSource = source;
+        this.mImage = image;
+        this.mTrackNumber = trackNumber;
+        this.mTotalTrackCount = totalTrackCount;
+        this.mDuration = duration;
+    }
+
+    public String getTitle() {
+        return mTitle;
+    }
+
+    public void setTitle(String mTitle) {
+        this.mTitle = mTitle;
+    }
+
+    public String getAlbum() {
+        return mAlbum;
+    }
+
+    public void setAlbum(String mAlbum) {
+        this.mAlbum = mAlbum;
+    }
+
+    public String getArtist() {
+        return mArtist;
+    }
+
+    public void setArtist(String mArtist) {
+        this.mArtist = mArtist;
+    }
+
+    public String getGenre() {
+        return mGenre;
+    }
+
+    public void setGenre(String mGenre) {
+        this.mGenre = mGenre;
+    }
+
+    public String getSource() {
+        return mSource;
+    }
+
+    public void setSource(String mSource) {
+        this.mSource = mSource;
+    }
+
+    public String getImage() {
+        return mImage;
+    }
+
+    public void setImage(String mImage) {
+        this.mImage = mImage;
+    }
+
+    public int getTrackNumber() {
+        return mTrackNumber;
+    }
+
+    public void setTrackNumber(int mTrackNumber) {
+        this.mTrackNumber = mTrackNumber;
+    }
+
+    public int getTotalTrackCount() {
+        return mTotalTrackCount;
+    }
+
+    public void setTotalTrackCount(int mTotalTrackCount) {
+        this.mTotalTrackCount = mTotalTrackCount;
+    }
+
+    public int getDuration() {
+        return mDuration;
+    }
+
+    public void setDuration(int mDuration) {
+        this.mDuration = mDuration;
+    }
+
+    public String toString() {
+        return mTitle;
+    }
+
+}
diff --git a/tests/VoiceInteraction/AndroidManifest.xml b/tests/VoiceInteraction/AndroidManifest.xml
index 33f000d..c328b3c 100644
--- a/tests/VoiceInteraction/AndroidManifest.xml
+++ b/tests/VoiceInteraction/AndroidManifest.xml
@@ -1,6 +1,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
         package="com.android.test.voiceinteraction">
 
+    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
+
     <application>
         <activity android:name="VoiceInteractionMain" android:label="Voice Interaction"
                 android:theme="@android:style/Theme.Material">