MediaSession2: Initial commit of MediaLibraryService2

MediaLibraryService2 is the new name for the MediaBrowserService

Test: Run all MediaComponents tests once
Change-Id: I0a29c4015cd22b5fa4e4e0f55562afd865eea1d6
diff --git a/packages/MediaComponents/src/com/android/media/MediaBrowser2Impl.java b/packages/MediaComponents/src/com/android/media/MediaBrowser2Impl.java
index ebd2932..b517b4a 100644
--- a/packages/MediaComponents/src/com/android/media/MediaBrowser2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaBrowser2Impl.java
@@ -49,10 +49,20 @@
             try {
                 binder.getBrowserRoot(getControllerStub(), rootHints);
             } catch (RemoteException e) {
-                Log.w(TAG, "Cannot connect to the service or the session is gone", e);
+                // TODO(jaewan): Handle disconnect.
+                if (DEBUG) {
+                    Log.w(TAG, "Cannot connect to the service or the session is gone", e);
+                }
             }
         } else {
             Log.w(TAG, "Session isn't active", new IllegalStateException());
         }
     }
+
+    public void onGetRootResult(
+            final Bundle rootHints, final String rootMediaId, final Bundle rootExtra) {
+        getCallbackExecutor().execute(() -> {
+            mCallback.onGetRootResult(rootHints, rootMediaId, rootExtra);
+        });
+    }
 }
diff --git a/packages/MediaComponents/src/com/android/media/MediaController2Impl.java b/packages/MediaComponents/src/com/android/media/MediaController2Impl.java
index 6cf11fa..7a01b97 100644
--- a/packages/MediaComponents/src/com/android/media/MediaController2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaController2Impl.java
@@ -159,6 +159,9 @@
 
     @Override
     public void release_impl() {
+        if (DEBUG) {
+            Log.d(TAG, "release from " + mToken);
+        }
         final IMediaSession2 binder;
         synchronized (mLock) {
             if (mIsReleased) {
@@ -196,6 +199,10 @@
         return mSessionCallbackStub;
     }
 
+    Executor getCallbackExecutor() {
+        return mCallbackExecutor;
+    }
+
     @Override
     public SessionToken getSessionToken_impl() {
         return mToken;
@@ -343,7 +350,8 @@
     private void onConnectionChangedNotLocked(IMediaSession2 sessionBinder,
             CommandGroup commandGroup) {
         if (DEBUG) {
-            Log.d(TAG, "onConnectionChangedNotLocked sessionBinder=" + sessionBinder);
+            Log.d(TAG, "onConnectionChangedNotLocked sessionBinder=" + sessionBinder
+                    + ", commands=" + commandGroup);
         }
         boolean release = false;
         try {
@@ -369,6 +377,9 @@
                     // so can be used without worrying about deadlock.
                     mSessionBinder.asBinder().linkToDeath(mDeathRecipient, 0);
                 } catch (RemoteException e) {
+                    if (DEBUG) {
+                        Log.d(TAG, "Session died too early.", e);
+                    }
                     release = true;
                     return;
                 }
@@ -390,7 +401,8 @@
         }
     }
 
-    // TODO(jaewan): Pull out this from the controller2, and rename it to the MediaBrowserStub
+    // TODO(jaewan): Pull out this from the controller2, and rename it to the MediaController2Stub
+    //               or MediaBrowser2Stub.
     static class MediaSession2CallbackStub extends IMediaSession2Callback.Stub {
         private final WeakReference<MediaController2Impl> mController;
 
@@ -406,6 +418,15 @@
             return controller;
         }
 
+        // TODO(jaewan): Refactor code to get rid of these pattern.
+        private MediaBrowser2Impl getBrowser() throws IllegalStateException {
+            final MediaController2Impl controller = getController();
+            if (controller instanceof MediaBrowser2Impl) {
+                return (MediaBrowser2Impl) controller;
+            }
+            return null;
+        }
+
         public void destroy() {
             mController.clear();
         }
@@ -429,6 +450,19 @@
             controller.onConnectionChangedNotLocked(
                     sessionBinder, CommandGroup.fromBundle(commandGroup));
         }
+
+        @Override
+        public void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra)
+                throws RuntimeException {
+            final MediaBrowser2Impl browser;
+            try {
+                browser = getBrowser();
+            } catch (IllegalStateException e) {
+                Log.w(TAG, "Don't fail silently here. Highly likely a bug");
+                return;
+            }
+            browser.onGetRootResult(rootHints, rootMediaId, rootExtra);
+        }
     }
 
     // This will be called on the main thread.
diff --git a/packages/MediaComponents/src/com/android/media/MediaLibraryService2Impl.java b/packages/MediaComponents/src/com/android/media/MediaLibraryService2Impl.java
new file mode 100644
index 0000000..430ab4c
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/media/MediaLibraryService2Impl.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.media;
+
+import android.content.Intent;
+import android.media.MediaLibraryService2;
+import android.media.MediaLibraryService2.MediaLibrarySession;
+import android.media.MediaSession2;
+import android.media.MediaSessionService2;
+import android.media.update.MediaLibraryService2Provider;
+
+public class MediaLibraryService2Impl extends MediaSessionService2Impl implements
+        MediaLibraryService2Provider {
+    private final MediaSessionService2 mInstance;
+    private MediaLibrarySession mLibrarySession;
+
+    public MediaLibraryService2Impl(MediaLibraryService2 instance) {
+        super(instance);
+        mInstance = instance;
+    }
+
+    @Override
+    public void onCreate_impl() {
+        super.onCreate_impl();
+
+        // Effectively final
+        MediaSession2 session = getSession();
+        if (!(session instanceof MediaLibrarySession)) {
+            throw new RuntimeException("Expected MediaLibrarySession, but returned MediaSession2");
+        }
+        mLibrarySession = (MediaLibrarySession) getSession();
+    }
+
+    @Override
+    Intent createServiceIntent() {
+        Intent serviceIntent = new Intent(mInstance, mInstance.getClass());
+        serviceIntent.setAction(MediaLibraryService2.SERVICE_INTERFACE);
+        return serviceIntent;
+    }
+}
diff --git a/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java b/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java
index 09d7adc..2cb562f 100644
--- a/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaSession2Impl.java
@@ -48,7 +48,6 @@
     private final Context mContext;
     private final String mId;
     private final Handler mHandler;
-    private final SessionCallback mCallback;
     private final MediaSession2Stub mSessionStub;
     private final SessionToken mSessionToken;
 
@@ -76,8 +75,7 @@
         mContext = context;
         mId = id;
         mHandler = new Handler(Looper.myLooper());
-        mCallback = callback;
-        mSessionStub = new MediaSession2Stub(this);
+        mSessionStub = new MediaSession2Stub(this, callback);
         // Ask server to create session token for following reasons.
         //   1. Make session ID unique per package.
         //      Server can only know if the package has another process and has another session
@@ -284,10 +282,6 @@
         return mInstance;
     }
 
-    SessionCallback getCallback() {
-        return mCallback;
-    }
-
     MediaPlayerBase getPlayer() {
         return mPlayer;
     }
@@ -420,5 +414,9 @@
         public void removeFlag(int flag) {
             mFlag &= ~flag;
         }
+
+        public static ControllerInfoImpl from(ControllerInfo controller) {
+            return (ControllerInfoImpl) controller.getProvider();
+        }
     }
 }
diff --git a/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java b/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java
index 9165794..8fb72c4 100644
--- a/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java
+++ b/packages/MediaComponents/src/com/android/media/MediaSession2Stub.java
@@ -21,10 +21,12 @@
 import android.content.Context;
 import android.media.IMediaSession2;
 import android.media.IMediaSession2Callback;
+import android.media.MediaLibraryService2.MediaLibrarySessionCallback;
 import android.media.MediaSession2;
 import android.media.MediaSession2.Command;
 import android.media.MediaSession2.CommandGroup;
 import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.SessionCallback;
 import android.media.session.PlaybackState;
 import android.os.Binder;
 import android.os.Bundle;
@@ -33,6 +35,7 @@
 import android.os.Looper;
 import android.os.Message;
 import android.os.RemoteException;
+import android.service.media.MediaBrowserService.BrowserRoot;
 import android.support.annotation.GuardedBy;
 import android.util.ArrayMap;
 import android.util.Log;
@@ -41,9 +44,6 @@
 import java.util.ArrayList;
 import java.util.List;
 
-// TODO(jaewan): Add a hook for media apps to log which app requested specific command.
-// TODO(jaewan): Add a way to block specific command from a specific app. Also add supported
-// command per apps.
 public class MediaSession2Stub extends IMediaSession2.Stub {
     private static final String TAG = "MediaSession2Stub";
     private static final boolean DEBUG = true; // TODO(jaewan): Rename.
@@ -52,14 +52,20 @@
     private final CommandHandler mCommandHandler;
     private final WeakReference<MediaSession2Impl> mSession;
     private final Context mContext;
+    private final SessionCallback mSessionCallback;
+    private final MediaLibrarySessionCallback mLibraryCallback;
 
     @GuardedBy("mLock")
     private final ArrayMap<IBinder, ControllerInfo> mControllers = new ArrayMap<>();
 
-    public MediaSession2Stub(MediaSession2Impl session) {
+    public MediaSession2Stub(MediaSession2Impl session, SessionCallback callback) {
         mSession = new WeakReference<>(session);
         mContext = session.getContext();
+        // TODO(jaewan): Should be executor from the session builder
         mCommandHandler = new CommandHandler(session.getHandler().getLooper());
+        mSessionCallback = callback;
+        mLibraryCallback = (callback instanceof MediaLibrarySessionCallback)
+                ? (MediaLibrarySessionCallback) callback : null;
     }
 
     public void destroyNotLocked() {
@@ -127,7 +133,20 @@
     @Override
     public void getBrowserRoot(IMediaSession2Callback caller, Bundle rootHints)
             throws RuntimeException {
-        // TODO(jaewan): Implement this.
+        if (mLibraryCallback == null) {
+            if (DEBUG) {
+                Log.d(TAG, "Session cannot hand getBrowserRoot()");
+            }
+            return;
+        }
+        final ControllerInfo controller = getController(caller);
+        if (controller == null) {
+            if (DEBUG) {
+                Log.d(TAG, "getBrowerRoot from a controller that hasn't connected. Ignore");
+            }
+            return;
+        }
+        mCommandHandler.postOnGetRoot(controller, rootHints);
     }
 
     @Deprecated
@@ -148,7 +167,7 @@
             if (controllerInfo == null) {
                 return;
             }
-            ((ControllerInfoImpl) controllerInfo.getProvider()).addFlag(callbackFlag);
+            ControllerInfoImpl.from(controllerInfo).addFlag(callbackFlag);
         }
     }
 
@@ -162,9 +181,7 @@
             if (controllerInfo == null) {
                 return;
             }
-            ControllerInfoImpl impl =
-                    ((ControllerInfoImpl) controllerInfo.getProvider());
-            impl.removeFlag(callbackFlag);
+            ControllerInfoImpl.from(controllerInfo).removeFlag(callbackFlag);
         }
     }
 
@@ -189,7 +206,7 @@
         synchronized (mLock) {
             for (int i = 0; i < mControllers.size(); i++) {
                 ControllerInfo controllerInfo = mControllers.valueAt(i);
-                if (((ControllerInfoImpl) controllerInfo.getProvider()).containsFlag(flag)) {
+                if (ControllerInfoImpl.from(controllerInfo).containsFlag(flag)) {
                     controllers.add(controllerInfo);
                 }
             }
@@ -202,8 +219,7 @@
         final List<ControllerInfo> list = getControllersWithFlag(CALLBACK_FLAG_PLAYBACK);
         for (int i = 0; i < list.size(); i++) {
             IMediaSession2Callback callbackBinder =
-                    ((ControllerInfoImpl) list.get(i).getProvider())
-                            .getControllerBinder();
+                    ControllerInfoImpl.from(list.get(i)).getControllerBinder();
             try {
                 callbackBinder.onPlaybackStateChanged(state);
             } catch (RemoteException e) {
@@ -213,9 +229,11 @@
         }
     }
 
+    // TODO(jaewan): Remove this. We should use Executor given by the session builder.
     private class CommandHandler extends Handler {
         public static final int MSG_CONNECT = 1000;
         public static final int MSG_COMMAND = 1001;
+        public static final int MSG_ON_GET_ROOT = 2000;
 
         public CommandHandler(Looper looper) {
             super(looper);
@@ -229,18 +247,22 @@
             }
 
             switch (msg.what) {
-                case MSG_CONNECT:
+                case MSG_CONNECT: {
                     ControllerInfo request = (ControllerInfo) msg.obj;
-                    CommandGroup allowedCommands = session.getCallback().onConnect(request);
+                    CommandGroup allowedCommands = mSessionCallback.onConnect(request);
                     // Don't reject connection for the request from trusted app.
                     // Otherwise server will fail to retrieve session's information to dispatch
                     // media keys to.
                     boolean accept = allowedCommands != null || request.isTrusted();
-                    ControllerInfoImpl impl = (ControllerInfoImpl) request.getProvider();
+                    ControllerInfoImpl impl = ControllerInfoImpl.from(request);
                     if (accept) {
                         synchronized (mLock) {
                             mControllers.put(impl.getId(), request);
                         }
+                        if (allowedCommands == null) {
+                            // For trusted apps, send non-null allowed commands to keep connection.
+                            allowedCommands = new CommandGroup();
+                        }
                     }
                     if (DEBUG) {
                         Log.d(TAG, "onConnectResult, request=" + request
@@ -254,10 +276,11 @@
                         // Controller may be died prematurely.
                     }
                     break;
-                case MSG_COMMAND:
+                }
+                case MSG_COMMAND: {
                     CommandParam param = (CommandParam) msg.obj;
                     Command command = param.command;
-                    boolean accepted = session.getCallback().onCommandRequest(
+                    boolean accepted = mSessionCallback.onCommandRequest(
                             param.controller, command);
                     if (!accepted) {
                         // Don't run rejected command.
@@ -288,6 +311,21 @@
                             // TODO(jaewan): Handle custom command.
                     }
                     break;
+                }
+                case MSG_ON_GET_ROOT: {
+                    final CommandParam param = (CommandParam) msg.obj;
+                    final ControllerInfoImpl controller = ControllerInfoImpl.from(param.controller);
+                    BrowserRoot root = mLibraryCallback.onGetRoot(param.controller, param.args);
+                    try {
+                        controller.getControllerBinder().onGetRootResult(param.args,
+                                root == null ? null : root.getRootId(),
+                                root == null ? null : root.getExtras());
+                    } catch (RemoteException e) {
+                        // Controller may be died prematurely.
+                        // TODO(jaewan): Handle this.
+                    }
+                    break;
+                }
             }
         }
 
@@ -299,6 +337,11 @@
             CommandParam param = new CommandParam(controller, command, args);
             obtainMessage(MSG_COMMAND, param).sendToTarget();
         }
+
+        public void postOnGetRoot(ControllerInfo controller, Bundle rootHints) {
+            CommandParam param = new CommandParam(controller, null, rootHints);
+            obtainMessage(MSG_ON_GET_ROOT, param).sendToTarget();
+        }
     }
 
     private static class CommandParam {
diff --git a/packages/MediaComponents/src/com/android/media/MediaSessionService2Impl.java b/packages/MediaComponents/src/com/android/media/MediaSessionService2Impl.java
index 6a4760d..773a06f 100644
--- a/packages/MediaComponents/src/com/android/media/MediaSessionService2Impl.java
+++ b/packages/MediaComponents/src/com/android/media/MediaSessionService2Impl.java
@@ -17,11 +17,7 @@
 package com.android.media;
 
 import static android.content.Context.NOTIFICATION_SERVICE;
-import static android.media.MediaSessionService2.DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID;
-import static android.media.MediaSessionService2.DEFAULT_MEDIA_NOTIFICATION_ID;
 
-import android.app.Notification;
-import android.app.NotificationChannel;
 import android.app.NotificationManager;
 import android.content.Intent;
 import android.content.pm.PackageManager;
@@ -37,7 +33,7 @@
 import android.support.annotation.GuardedBy;
 import android.util.Log;
 
-// Need a test for session service itself.
+// TODO(jaewan): Need a test for session service itself.
 public class MediaSessionService2Impl implements MediaSessionService2Provider {
 
     private static final String TAG = "MPSessionService"; // to meet 23 char limit in Log tag
@@ -53,7 +49,6 @@
     private Intent mStartSelfIntent;
 
     private boolean mIsRunningForeground;
-    private NotificationChannel mDefaultNotificationChannel;
     private MediaSession2 mSession;
 
     public MediaSessionService2Impl(MediaSessionService2 instance) {
@@ -65,6 +60,10 @@
 
     @Override
     public MediaSession2 getSession_impl() {
+        return getSession();
+    }
+
+    MediaSession2 getSession() {
         synchronized (mLock) {
             return mSession;
         }
@@ -72,43 +71,23 @@
 
     @Override
     public MediaNotification onUpdateNotification_impl(PlaybackState state) {
-        return createDefaultNotification(state);
+        // Provide default notification UI later.
+        return null;
     }
 
-    // TODO(jaewan): Remove this for framework release.
-    private MediaNotification createDefaultNotification(PlaybackState state) {
-        // TODO(jaewan): Place better notification here.
-        if (mDefaultNotificationChannel == null) {
-            mDefaultNotificationChannel = new NotificationChannel(
-                    DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID,
-                    DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID,
-                    NotificationManager.IMPORTANCE_DEFAULT);
-            mNotificationManager.createNotificationChannel(mDefaultNotificationChannel);
-        }
-        Notification notification = new Notification.Builder(
-                mInstance, DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID)
-                .setContentTitle(mInstance.getPackageName())
-                .setContentText("Playback state: " + state.getState())
-                .setSmallIcon(android.R.drawable.sym_def_app_icon).build();
-        return MediaNotification.create(DEFAULT_MEDIA_NOTIFICATION_ID, notification);
-    }
-
-
     @Override
     public void onCreate_impl() {
         mNotificationManager = (NotificationManager) mInstance.getSystemService(
                 NOTIFICATION_SERVICE);
         mStartSelfIntent = new Intent(mInstance, mInstance.getClass());
 
-        Intent serviceIntent = new Intent(mInstance, mInstance.getClass());
-        serviceIntent.setAction(MediaSessionService2.SERVICE_INTERFACE);
+        Intent serviceIntent = createServiceIntent();
         ResolveInfo resolveInfo = mInstance.getPackageManager()
-                .resolveService(serviceIntent,
-                        PackageManager.GET_META_DATA);
+                .resolveService(serviceIntent, PackageManager.GET_META_DATA);
         String id;
         if (resolveInfo == null || resolveInfo.serviceInfo == null) {
             throw new IllegalArgumentException("service " + mInstance + " doesn't implement"
-                    + MediaSessionService2.SERVICE_INTERFACE);
+                    + serviceIntent.getAction());
         } else if (resolveInfo.serviceInfo.metaData == null) {
             if (DEBUG) {
                 Log.d(TAG, "Failed to resolve ID for " + mInstance + ". Using empty id");
@@ -122,6 +101,14 @@
         if (mSession == null || !id.equals(mSession.getToken().getId())) {
             throw new RuntimeException("Expected session with id " + id + ", but got " + mSession);
         }
+        // TODO(jaewan): Uncomment here.
+        // mSession.addPlaybackListener(mListener, mSession.getExecutor());
+    }
+
+    Intent createServiceIntent() {
+        Intent serviceIntent = new Intent(mInstance, mInstance.getClass());
+        serviceIntent.setAction(MediaSessionService2.SERVICE_INTERFACE);
+        return serviceIntent;
     }
 
     public IBinder onBind_impl(Intent intent) {
@@ -134,7 +121,7 @@
     private void updateNotification(PlaybackState state) {
         MediaNotification mediaNotification = mInstance.onUpdateNotification(state);
         if (mediaNotification == null) {
-            mediaNotification = createDefaultNotification(state);
+            return;
         }
         switch((int) state.getState()) {
             case PlaybackState.STATE_PLAYING:
diff --git a/packages/MediaComponents/src/com/android/media/update/ApiFactory.java b/packages/MediaComponents/src/com/android/media/update/ApiFactory.java
index 6213b32..07b565e 100644
--- a/packages/MediaComponents/src/com/android/media/update/ApiFactory.java
+++ b/packages/MediaComponents/src/com/android/media/update/ApiFactory.java
@@ -22,6 +22,7 @@
 import android.media.MediaBrowser2;
 import android.media.MediaBrowser2.BrowserCallback;
 import android.media.MediaController2;
+import android.media.MediaLibraryService2;
 import android.media.MediaPlayerBase;
 import android.media.MediaSession2;
 import android.media.MediaSession2.ControllerInfo;
@@ -44,6 +45,7 @@
 
 import com.android.media.MediaBrowser2Impl;
 import com.android.media.MediaController2Impl;
+import com.android.media.MediaLibraryService2Impl;
 import com.android.media.MediaSession2Impl;
 import com.android.media.MediaSessionService2Impl;
 import com.android.widget.MediaControlView2Impl;
@@ -92,6 +94,12 @@
     }
 
     @Override
+    public MediaSessionService2Provider createMediaLibraryService2(
+            MediaLibraryService2 instance) {
+        return new MediaLibraryService2Impl(instance);
+    }
+
+    @Override
     public MediaControlView2Provider createMediaControlView2(
             MediaControlView2 instance, ViewProvider superProvider) {
         return new MediaControlView2Impl(instance, superProvider);
diff --git a/packages/MediaComponents/test/AndroidManifest.xml b/packages/MediaComponents/test/AndroidManifest.xml
index fe16583..30bac87 100644
--- a/packages/MediaComponents/test/AndroidManifest.xml
+++ b/packages/MediaComponents/test/AndroidManifest.xml
@@ -32,10 +32,17 @@
         <!-- Keep the test services synced together with the TestUtils.java -->
         <service android:name="android.media.MockMediaSessionService2">
             <intent-filter>
-                <action android:name="android.media.session.MediaSessionService2" />
+                <action android:name="android.media.MediaSessionService2" />
             </intent-filter>
             <meta-data android:name="android.media.session" android:value="TestSession" />
         </service>
+        <!-- Keep the test services synced together with the MockMediaLibraryService -->
+        <service android:name="android.media.MockMediaLibraryService2">
+            <intent-filter>
+                <action android:name="android.media.MediaLibraryService2" />
+            </intent-filter>
+            <meta-data android:name="android.media.session" android:value="TestBrowser" />
+        </service>
     </application>
 
     <instrumentation
diff --git a/packages/MediaComponents/test/src/android/media/MediaBrowser2Test.java b/packages/MediaComponents/test/src/android/media/MediaBrowser2Test.java
index 2d8ca8e..fe8aeb9 100644
--- a/packages/MediaComponents/test/src/android/media/MediaBrowser2Test.java
+++ b/packages/MediaComponents/test/src/android/media/MediaBrowser2Test.java
@@ -16,6 +16,7 @@
 
 package android.media;
 
+import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
@@ -28,6 +29,7 @@
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 
+import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.util.concurrent.CountDownLatch;
@@ -43,7 +45,7 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class MediaBrowser2Test extends MediaController2Test {
-    private static final String TAG = "MediaPlayerBrowserTest";
+    private static final String TAG = "MediaBrowser2Test";
 
     @Override
     TestControllerInterface onCreateController(@NonNull SessionToken token,
@@ -51,6 +53,29 @@
         return new TestMediaBrowser(mContext, token, new TestBrowserCallback(callback));
     }
 
+    @Test
+    public void testGetBrowserRoot() throws InterruptedException {
+        final Bundle param = new Bundle();
+        param.putString(TAG, TAG);
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final TestControllerCallbackInterface callback = new TestControllerCallbackInterface() {
+            @Override
+            public void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra) {
+                assertTrue(TestUtils.equals(param, rootHints));
+                assertEquals(MockMediaLibraryService2.ROOT_ID, rootMediaId);
+                assertTrue(TestUtils.equals(MockMediaLibraryService2.EXTRA, rootExtra));
+                latch.countDown();
+            }
+        };
+
+        final SessionToken token = MockMediaLibraryService2.getToken(mContext);
+        MediaBrowser2 browser =
+                (MediaBrowser2) createController(token,true, callback);
+        browser.getBrowserRoot(param);
+        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+    }
+
     public static class TestBrowserCallback extends BrowserCallback
             implements WaitForConnectionInterface {
         private final TestControllerCallbackInterface mCallbackProxy;
@@ -77,7 +102,7 @@
 
         @Override
         public void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra) {
-            // No-op here.
+            mCallbackProxy.onGetRootResult(rootHints, rootMediaId, rootExtra);
         }
 
         @Override
diff --git a/packages/MediaComponents/test/src/android/media/MediaController2Test.java b/packages/MediaComponents/test/src/android/media/MediaController2Test.java
index 2aeb4ef..88ca106 100644
--- a/packages/MediaComponents/test/src/android/media/MediaController2Test.java
+++ b/packages/MediaComponents/test/src/android/media/MediaController2Test.java
@@ -51,9 +51,9 @@
 public class MediaController2Test extends MediaSession2TestBase {
     private static final String TAG = "MediaController2Test";
 
-    private MediaSession2 mSession;
-    private MediaController2 mController;
-    private MockPlayer mPlayer;
+    MediaSession2 mSession;
+    MediaController2 mController;
+    MockPlayer mPlayer;
 
     @Before
     @Override
@@ -331,11 +331,23 @@
         mPlayer = (MockPlayer) mSession.getPlayer();
     }
 
+    // TODO(jaewan): Reenable when session manager detects app installs
     @Ignore
     @Test
-    public void testConnectToService() throws InterruptedException {
+    public void testConnectToService_sessionService() throws InterruptedException {
         connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
+        testConnectToService();
+    }
 
+    // TODO(jaewan): Reenable when session manager detects app installs
+    @Ignore
+    @Test
+    public void testConnectToService_libraryService() throws InterruptedException {
+        connectToService(TestUtils.getServiceToken(mContext, MockMediaLibraryService2.ID));
+        testConnectToService();
+    }
+
+    public void testConnectToService() throws InterruptedException {
         TestServiceRegistry serviceInfo = TestServiceRegistry.getInstance();
         ControllerInfo info = serviceInfo.getOnConnectControllerInfo();
         assertEquals(mContext.getPackageName(), info.getPackageName());
@@ -396,10 +408,24 @@
         testControllerAfterSessionIsGone(id);
     }
 
+    // TODO(jaewan): Reenable when session manager detects app installs
     @Ignore
     @Test
     public void testRelease_sessionService() throws InterruptedException {
         connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
+        testReleaseFromService();
+    }
+
+    // TODO(jaewan): Reenable when session manager detects app installs
+    @Ignore
+    @Test
+    public void testRelease_libraryService() throws InterruptedException {
+        connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
+        testReleaseFromService();
+    }
+
+    private void testReleaseFromService() throws InterruptedException {
+        final String id = mController.getSessionToken().getId();
         final CountDownLatch latch = new CountDownLatch(1);
         TestServiceRegistry.getInstance().setServiceInstanceChangedCallback((service) -> {
             if (service == null) {
@@ -415,7 +441,7 @@
 
         // Test whether the controller is notified about later release of the session or
         // re-creation.
-        testControllerAfterSessionIsGone(MockMediaSessionService2.ID);
+        testControllerAfterSessionIsGone(id);
     }
 
     private void testControllerAfterSessionIsGone(final String id) throws InterruptedException {
@@ -435,7 +461,6 @@
         testNoInteraction();
     }
 
-
     private void testNoInteraction() throws InterruptedException {
         final CountDownLatch latch = new CountDownLatch(1);
         final PlaybackListener playbackListener = (state) -> {
diff --git a/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java b/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java
index ffa3c64..cc77a50 100644
--- a/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java
+++ b/packages/MediaComponents/test/src/android/media/MediaSession2TestBase.java
@@ -22,6 +22,7 @@
 import android.content.Context;
 import android.media.MediaController2.ControllerCallback;
 import android.media.MediaSession2.CommandGroup;
+import android.os.Bundle;
 import android.os.HandlerThread;
 import android.support.annotation.CallSuper;
 import android.support.annotation.NonNull;
@@ -57,6 +58,9 @@
 
     interface TestControllerCallbackInterface {
         // Currently empty. Add methods in ControllerCallback/BrowserCallback that you want to test.
+
+        // Browser specific callbacks
+        default void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra) {}
     }
 
     interface WaitForConnectionInterface {
diff --git a/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java b/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java
index 0ee37b1..df45063 100644
--- a/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java
+++ b/packages/MediaComponents/test/src/android/media/MediaSessionManager_MediaSession2.java
@@ -158,6 +158,7 @@
     @Test
     public void testGetMediaSessionService2Token() throws InterruptedException {
         boolean foundTestSessionService = false;
+        boolean foundTestLibraryService = false;
         List<SessionToken> tokens = mManager.getSessionServiceTokens();
         for (int i = 0; i < tokens.size(); i++) {
             SessionToken token = tokens.get(i);
@@ -167,15 +168,23 @@
                 assertEquals(SessionToken.TYPE_SESSION_SERVICE, token.getType());
                 assertNull(token.getSessionBinder());
                 foundTestSessionService = true;
+            } else if (mContext.getPackageName().equals(token.getPackageName())
+                    && MockMediaLibraryService2.ID.equals(token.getId())) {
+                assertFalse(foundTestLibraryService);
+                assertEquals(SessionToken.TYPE_LIBRARY_SERVICE, token.getType());
+                assertNull(token.getSessionBinder());
+                foundTestLibraryService = true;
             }
         }
         assertTrue(foundTestSessionService);
+        assertTrue(foundTestLibraryService);
     }
 
     @Test
     public void testGetAllSessionTokens() throws InterruptedException {
         boolean foundTestSession = false;
         boolean foundTestSessionService = false;
+        boolean foundTestLibraryService = false;
         List<SessionToken> tokens = mManager.getAllSessionTokens();
         for (int i = 0; i < tokens.size(); i++) {
             SessionToken token = tokens.get(i);
@@ -192,12 +201,18 @@
                     foundTestSessionService = true;
                     assertEquals(SessionToken.TYPE_SESSION_SERVICE, token.getType());
                     break;
+                case MockMediaLibraryService2.ID:
+                    assertFalse(foundTestLibraryService);
+                    assertEquals(SessionToken.TYPE_LIBRARY_SERVICE, token.getType());
+                    foundTestLibraryService = true;
+                    break;
                 default:
                     fail("Unexpected session " + token + " exists in the package");
             }
         }
         assertTrue(foundTestSession);
         assertTrue(foundTestSessionService);
+        assertTrue(foundTestLibraryService);
     }
 
     // Ensures if the session creation/release is notified to the server.
diff --git a/packages/MediaComponents/test/src/android/media/MockMediaLibraryService2.java b/packages/MediaComponents/test/src/android/media/MockMediaLibraryService2.java
new file mode 100644
index 0000000..7a16127
--- /dev/null
+++ b/packages/MediaComponents/test/src/android/media/MockMediaLibraryService2.java
@@ -0,0 +1,99 @@
+/*
+* Copyright 2018 The Android Open Source Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*      http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+package android.media;
+
+import static junit.framework.Assert.fail;
+
+import android.content.Context;
+import android.media.MediaSession2.CommandGroup;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.TestUtils.SyncHandler;
+import android.os.Bundle;
+import android.os.Process;
+import android.service.media.MediaBrowserService.BrowserRoot;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Mock implementation of {@link MediaLibraryService2} for testing.
+ */
+public class MockMediaLibraryService2 extends MediaLibraryService2 {
+    // Keep in sync with the AndroidManifest.xml
+    public static final String ID = "TestLibrary";
+
+    public static final String ROOT_ID = "rootId";
+    public static final Bundle EXTRA = new Bundle();
+    static {
+        EXTRA.putString(ROOT_ID, ROOT_ID);
+    }
+    @GuardedBy("MockMediaLibraryService2.class")
+    private static SessionToken sToken;
+
+    private MediaLibrarySession mSession;
+
+    @Override
+    public MediaLibrarySession onCreateSession(String sessionId) {
+        final MockPlayer player = new MockPlayer(1);
+        SyncHandler handler = (SyncHandler) TestServiceRegistry.getInstance().getHandler();
+        try {
+            handler.postAndSync(() -> {
+                TestLibrarySessionCallback callback = new TestLibrarySessionCallback();
+                mSession = new MediaLibrarySessionBuilder(
+                        MockMediaLibraryService2.this, player, callback)
+                        .setId(sessionId).build();
+            });
+        } catch (InterruptedException e) {
+            fail(e.toString());
+        }
+        return mSession;
+    }
+
+    @Override
+    public void onDestroy() {
+        TestServiceRegistry.getInstance().cleanUp();
+        super.onDestroy();
+    }
+
+    public static SessionToken getToken(Context context) {
+        synchronized (MockMediaLibraryService2.class) {
+            if (sToken == null) {
+                sToken = new SessionToken(SessionToken.TYPE_LIBRARY_SERVICE,
+                        context.getPackageName(), ID,
+                        MockMediaLibraryService2.class.getName(), null);
+            }
+            return sToken;
+        }
+    }
+
+    private class TestLibrarySessionCallback extends MediaLibrarySessionCallback {
+        @Override
+        public CommandGroup onConnect(ControllerInfo controller) {
+            if (Process.myUid() != controller.getUid()) {
+                // It's system app wants to listen changes. Ignore.
+                return super.onConnect(controller);
+            }
+            TestServiceRegistry.getInstance().setServiceInstance(
+                    MockMediaLibraryService2.this, controller);
+            return super.onConnect(controller);
+        }
+
+        @Override
+        public BrowserRoot onGetRoot(ControllerInfo controller, Bundle rootHints) {
+            return new BrowserRoot(ROOT_ID, EXTRA);
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java b/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java
index e4a7485..9cf4911 100644
--- a/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java
+++ b/packages/MediaComponents/test/src/android/media/MockMediaSessionService2.java
@@ -18,9 +18,14 @@
 
 import static junit.framework.Assert.fail;
 
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
 import android.media.MediaSession2.ControllerInfo;
 import android.media.MediaSession2.SessionCallback;
 import android.media.TestUtils.SyncHandler;
+import android.media.session.PlaybackState;
 import android.os.Process;
 
 /**
@@ -29,7 +34,13 @@
 public class MockMediaSessionService2 extends MediaSessionService2 {
     // Keep in sync with the AndroidManifest.xml
     public static final String ID = "TestSession";
-    public MediaSession2 mSession;
+
+    private static final String DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID = "media_session_service";
+    private static final int DEFAULT_MEDIA_NOTIFICATION_ID = 1001;
+
+    private NotificationChannel mDefaultNotificationChannel;
+    private MediaSession2 mSession;
+    private NotificationManager mNotificationManager;
 
     @Override
     public MediaSession2 onCreateSession(String sessionId) {
@@ -49,6 +60,7 @@
     @Override
     public void onCreate() {
         super.onCreate();
+        mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
     }
 
     @Override
@@ -57,6 +69,23 @@
         super.onDestroy();
     }
 
+    @Override
+    public MediaNotification onUpdateNotification(PlaybackState state) {
+        if (mDefaultNotificationChannel == null) {
+            mDefaultNotificationChannel = new NotificationChannel(
+                    DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID,
+                    DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID,
+                    NotificationManager.IMPORTANCE_DEFAULT);
+            mNotificationManager.createNotificationChannel(mDefaultNotificationChannel);
+        }
+        Notification notification = new Notification.Builder(
+                this, DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID)
+                .setContentTitle(getPackageName())
+                .setContentText("Playback state: " + state.getState())
+                .setSmallIcon(android.R.drawable.sym_def_app_icon).build();
+        return MediaNotification.create(DEFAULT_MEDIA_NOTIFICATION_ID, notification);
+    }
+
     private class MySessionCallback extends SessionCallback {
         @Override
         public MediaSession2.CommandGroup onConnect(ControllerInfo controller) {
diff --git a/packages/MediaComponents/test/src/android/media/TestUtils.java b/packages/MediaComponents/test/src/android/media/TestUtils.java
index 0cca12c..1372f01 100644
--- a/packages/MediaComponents/test/src/android/media/TestUtils.java
+++ b/packages/MediaComponents/test/src/android/media/TestUtils.java
@@ -19,11 +19,13 @@
 import android.content.Context;
 import android.media.session.MediaSessionManager;
 import android.media.session.PlaybackState;
+import android.os.Bundle;
 import android.os.Handler;
 
 import android.os.Looper;
 
 import java.util.List;
+import java.util.Objects;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -47,6 +49,14 @@
         return new PlaybackState.Builder().setState(state, 0, 1.0f).build();
     }
 
+    /**
+     * Finds the session with id in this test package.
+     *
+     * @param context
+     * @param id
+     * @return
+     */
+    // TODO(jaewan): Currently not working.
     public static SessionToken getServiceToken(Context context, String id) {
         MediaSessionManager manager =
                 (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);
@@ -63,6 +73,33 @@
     }
 
     /**
+     * Compares contents of two bundles.
+     *
+     * @param a a bundle
+     * @param b another bundle
+     * @return {@code true} if two bundles are the same. {@code false} otherwise. This may be
+     *     incorrect if any bundle contains a bundle.
+     */
+    public static boolean equals(Bundle a, Bundle b) {
+        if (a == b) {
+            return true;
+        }
+        if (a == null || b == null) {
+            return false;
+        }
+        if (!a.keySet().containsAll(b.keySet())
+                || !b.keySet().containsAll(a.keySet())) {
+            return false;
+        }
+        for (String key : a.keySet()) {
+            if (!Objects.equals(a.get(key), b.get(key))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
      * Handler that always waits until the Runnable finishes.
      */
     public static class SyncHandler extends Handler {