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/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 {