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 {