Add init MediaSession2 and MediaController2
This CL only covers the connection logic between MediaSession2
and MediaController2.
Bug: 122055262
Test: build
Change-Id: I1773aa053ea1fce0cc548f334416e47d373f7f86
diff --git a/Android.bp b/Android.bp
index 4211162..8c68355 100644
--- a/Android.bp
+++ b/Android.bp
@@ -461,11 +461,13 @@
"media/java/android/media/IAudioRoutesObserver.aidl",
"media/java/android/media/IAudioService.aidl",
"media/java/android/media/IAudioServerStateDispatcher.aidl",
+ "media/java/android/media/IMediaController2.aidl",
"media/java/android/media/IMediaHTTPConnection.aidl",
"media/java/android/media/IMediaHTTPService.aidl",
"media/java/android/media/IMediaResourceMonitor.aidl",
"media/java/android/media/IMediaRouterClient.aidl",
"media/java/android/media/IMediaRouterService.aidl",
+ "media/java/android/media/IMediaSession2.aidl",
"media/java/android/media/IMediaScannerListener.aidl",
"media/java/android/media/IMediaScannerService.aidl",
"media/java/android/media/IPlaybackConfigDispatcher.aidl",
diff --git a/media/java/android/media/Controller2Link.aidl b/media/java/android/media/Controller2Link.aidl
new file mode 100644
index 0000000..64edafc
--- /dev/null
+++ b/media/java/android/media/Controller2Link.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+parcelable Controller2Link;
diff --git a/media/java/android/media/Controller2Link.java b/media/java/android/media/Controller2Link.java
new file mode 100644
index 0000000..d6a6e14
--- /dev/null
+++ b/media/java/android/media/Controller2Link.java
@@ -0,0 +1,133 @@
+/*
+ * 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 android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+
+/**
+ * Handles incoming commands from {@link MediaSession2} and {@link MediaLibrarySession}
+ * to both {@link MediaController2} and {@link MediaBrowser2}.
+ * @hide
+ */
+// @SystemApi
+public final class Controller2Link implements Parcelable {
+ private static final String TAG = "Controller2Link";
+ private static final boolean DEBUG = MediaController2.DEBUG;
+
+ public static final Parcelable.Creator<Controller2Link> CREATOR =
+ new Parcelable.Creator<Controller2Link>() {
+ @Override
+ public Controller2Link createFromParcel(Parcel in) {
+ return new Controller2Link(in);
+ }
+
+ @Override
+ public Controller2Link[] newArray(int size) {
+ return new Controller2Link[size];
+ }
+ };
+
+
+ private final MediaController2 mController;
+ private final IMediaController2 mIController;
+
+ public Controller2Link(MediaController2 controller) {
+ mController = controller;
+ mIController = new Controller2Stub();
+ }
+
+ Controller2Link(Parcel in) {
+ mController = null;
+ mIController = IMediaController2.Stub.asInterface(in.readStrongBinder());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeStrongBinder(mIController.asBinder());
+ }
+
+ /** Interface method for IMediaController2.notifyConnected */
+ public void notifyConnected(int seq, Bundle connectionResult) {
+ try {
+ mIController.notifyConnected(seq, connectionResult);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Interface method for IMediaController2.notifyDisonnected */
+ public void notifyDisconnected(int seq) {
+ try {
+ mIController.notifyDisconnected(seq);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Interface method for IMediaController2.sendSessionCommand */
+ public void sendSessionCommand(int seq, Session2Command command, Bundle args) {
+ try {
+ mIController.sendSessionCommand(seq, command, args);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Stub implementation for IMediaController2.notifyConnected */
+ public void onConnected(int seq, Bundle connectionResult) {
+ if (connectionResult == null) {
+ onDisconnected(seq);
+ return;
+ }
+ mController.onConnected(seq, connectionResult);
+ }
+
+ /** Stub implementation for IMediaController2.notifyDisonnected */
+ public void onDisconnected(int seq) {
+ mController.onDisconnected(seq);
+ }
+
+ /** Stub implementation for IMediaController2.sendSessionCommand */
+ public void onSessionCommand(int seq, Session2Command command, Bundle args) {
+ mController.onSessionCommand(seq, command, args);
+ }
+
+ private class Controller2Stub extends IMediaController2.Stub {
+ @Override
+ public void notifyConnected(int seq, Bundle connectionResult) {
+ Controller2Link.this.onConnected(seq, connectionResult);
+ }
+
+ @Override
+ public void notifyDisconnected(int seq) {
+ Controller2Link.this.onDisconnected(seq);
+ }
+
+ @Override
+ public void sendSessionCommand(int seq, Session2Command command, Bundle args) {
+ Controller2Link.this.onSessionCommand(seq, command, args);
+ }
+ }
+}
diff --git a/media/java/android/media/IMediaController2.aidl b/media/java/android/media/IMediaController2.aidl
new file mode 100644
index 0000000..87399ab
--- /dev/null
+++ b/media/java/android/media/IMediaController2.aidl
@@ -0,0 +1,34 @@
+/*
+ * 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 android.os.Bundle;
+import android.media.Session2Command;
+
+/**
+ * Interface from MediaSession2 to MediaController2.
+ * <p>
+ * Keep this interface oneway. Otherwise a malicious app may implement fake version of this,
+ * and holds calls from session to make session owner(s) frozen.
+ * @hide
+ */
+ // Code for AML only
+oneway interface IMediaController2 {
+ void notifyConnected(int seq, in Bundle connectionResult) = 0;
+ void notifyDisconnected(int seq) = 1;
+ void sendSessionCommand(int seq, in Session2Command command, in Bundle args) = 2;
+}
diff --git a/media/java/android/media/IMediaSession2.aidl b/media/java/android/media/IMediaSession2.aidl
new file mode 100644
index 0000000..d52cfc9
--- /dev/null
+++ b/media/java/android/media/IMediaSession2.aidl
@@ -0,0 +1,37 @@
+/*
+ * 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 android.os.Bundle;
+import android.media.Controller2Link;
+import android.media.Session2Command;
+
+/**
+ * Interface from MediaController2 to MediaSession2.
+ * <p>
+ * Keep this interface oneway. Otherwise a malicious app may implement fake version of this,
+ * and holds calls from session to make session owner(s) frozen.
+ * @hide
+ */
+ // Code for AML only
+oneway interface IMediaSession2 {
+ void connect(in Controller2Link caller, int seq, in Bundle connectionRequest) = 0;
+ void disconnect(in Controller2Link caller, int seq) = 1;
+ void sendSessionCommand(in Controller2Link caller, int seq, in Session2Command sessionCommand,
+ in Bundle args) = 2;
+ // Next Id : 3
+}
diff --git a/media/java/android/media/MediaConstants.java b/media/java/android/media/MediaConstants.java
new file mode 100644
index 0000000..ffdca16d8
--- /dev/null
+++ b/media/java/android/media/MediaConstants.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+// Code for AML only
+class MediaConstants {
+ // Bundle key for int
+ static final String KEY_PID = "android.media.key.PID";
+
+ // Bundle key for String
+ static final String KEY_PACKAGE_NAME = "android.media.key.PACKAGE_NAME";
+
+ // Bundle key for Parcelable
+ static final String KEY_SESSION2_STUB = "android.media.key.SESSION2_STUB";
+ static final String KEY_ALLOWED_COMMANDS = "android.media.key.ALLOWED_COMMANDS";
+
+ private MediaConstants() {
+ }
+}
diff --git a/media/java/android/media/MediaController2.java b/media/java/android/media/MediaController2.java
new file mode 100644
index 0000000..4a09bf1
--- /dev/null
+++ b/media/java/android/media/MediaController2.java
@@ -0,0 +1,220 @@
+/*
+ * 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 android.media.MediaConstants.KEY_ALLOWED_COMMANDS;
+import static android.media.MediaConstants.KEY_PACKAGE_NAME;
+import static android.media.MediaConstants.KEY_PID;
+import static android.media.MediaConstants.KEY_SESSION2_STUB;
+import static android.media.Session2Token.TYPE_SESSION;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Process;
+import android.util.Log;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Allows an app to interact with an active {@link MediaSession2} or a
+ * {@link MediaSession2Service} which would provide {@link MediaSession2}. Media buttons and other
+ * commands can be sent to the session.
+ * <p>
+ * This API is not generally intended for third party application developers.
+ * Use the <a href="{@docRoot}tools/extras/support-library.html">Support Library</a>
+ * {@link androidx.media2.MediaController} for consistent behavior across all devices.
+ * @hide
+ */
+public class MediaController2 implements AutoCloseable {
+ static final String TAG = "MediaController2";
+ static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final ControllerCallback mCallback;
+
+ private final IBinder.DeathRecipient mDeathRecipient = () -> close();
+ private final Context mContext;
+ private final Session2Token mSessionToken;
+ private final Executor mCallbackExecutor;
+ private final Controller2Link mControllerStub;
+
+ private final Object mLock = new Object();
+ //@GuardedBy("mLock")
+ private int mNextSeqNumber;
+ //@GuardedBy("mLock")
+ private Session2Link mSessionBinder;
+ //@GuardedBy("mLock")
+ private Session2CommandGroup mAllowedCommands;
+ //@GuardedBy("mLock")
+ private Session2Token mConnectedToken;
+
+ /**
+ * Create a {@link MediaController2} from the {@link Session2Token}.
+ * This connects to the session and may wake up the service if it's not available.
+ *
+ * @param context Context
+ * @param token token to connect to
+ * @param executor executor to run callbacks on.
+ * @param callback controller callback to receive changes in
+ */
+ public MediaController2(@NonNull final Context context, @NonNull final Session2Token token,
+ @NonNull final Executor executor, @NonNull final ControllerCallback callback) {
+ if (context == null) {
+ throw new IllegalArgumentException("context shouldn't be null");
+ }
+ if (token == null) {
+ throw new IllegalArgumentException("token shouldn't be null");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("callback shouldn't be null");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("executor shouldn't be null");
+ }
+ mContext = context;
+ mSessionToken = token;
+ mCallbackExecutor = executor;
+ mCallback = callback;
+ mControllerStub = new Controller2Link(this);
+
+ mNextSeqNumber = 0;
+
+ Session2Link sessionBinder = token.getSessionLink();
+ if (token.getType() == TYPE_SESSION) {
+ connectToSession();
+ } else {
+ // TODO: Handle connect to session service.
+ }
+ }
+
+ @Override
+ public void close() {
+ synchronized (mLock) {
+ if (mSessionBinder != null) {
+ try {
+ mSessionBinder.unlinkToDeath(mDeathRecipient, 0);
+ mSessionBinder.disconnect(mControllerStub, mNextSeqNumber++);
+ } catch (RuntimeException e) {
+ // No-op
+ }
+ }
+ mCallbackExecutor.execute(() -> {
+ mCallback.onDisconnected(MediaController2.this);
+ });
+ }
+ }
+
+ void onConnected(int seq, Bundle connectionResult) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ Session2Link sessionBinder = connectionResult.getParcelable(KEY_SESSION2_STUB);
+ Session2CommandGroup allowedCommands =
+ connectionResult.getParcelable(KEY_ALLOWED_COMMANDS);
+ if (DEBUG) {
+ Log.d(TAG, "notifyConnected sessionBinder=" + sessionBinder
+ + ", allowedCommands=" + allowedCommands);
+ }
+ if (sessionBinder == null || allowedCommands == null) {
+ // Connection rejected.
+ close();
+ return;
+ }
+ synchronized (mLock) {
+ mSessionBinder = sessionBinder;
+ mAllowedCommands = allowedCommands;
+ // Implementation for the local binder is no-op,
+ // so can be used without worrying about deadlock.
+ sessionBinder.linkToDeath(mDeathRecipient, 0);
+ mConnectedToken = new Session2Token(mSessionToken.getUid(), TYPE_SESSION,
+ mSessionToken.getPackageName(), sessionBinder);
+ }
+ mCallbackExecutor.execute(() -> {
+ mCallback.onConnected(MediaController2.this, allowedCommands);
+ });
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ void onDisconnected(int seq) {
+ // TODO: Implement this
+ }
+
+ void onSessionCommand(int seq, Session2Command command, Bundle args) {
+ // TODO: Implement this
+ }
+
+ private int getNextSeqNumber() {
+ synchronized (mLock) {
+ return mNextSeqNumber++;
+ }
+ }
+
+ private void connectToSession() {
+ Session2Link sessionBinder = mSessionToken.getSessionLink();
+ Bundle connectionRequest = new Bundle();
+ connectionRequest.putString(KEY_PACKAGE_NAME, mContext.getPackageName());
+ connectionRequest.putInt(KEY_PID, Process.myPid());
+
+ try {
+ sessionBinder.connect(mControllerStub, getNextSeqNumber(), connectionRequest);
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Failed to call connection request. Framework will retry"
+ + " automatically");
+ }
+ }
+
+ /**
+ * Interface for listening to change in activeness of the {@link MediaSession2}.
+ * <p>
+ * This API is not generally intended for third party application developers.
+ */
+ public abstract static class ControllerCallback {
+ /**
+ * Called when the controller is successfully connected to the session. The controller
+ * becomes available afterwards.
+ *
+ * @param controller the controller for this event
+ * @param allowedCommands commands that's allowed by the session.
+ */
+ public void onConnected(@NonNull MediaController2 controller,
+ @NonNull Session2CommandGroup allowedCommands) { }
+
+ /**
+ * Called when the session refuses the controller or the controller is disconnected from
+ * the session. The controller becomes unavailable afterwards and the callback wouldn't
+ * be called.
+ * <p>
+ * It will be also called after the {@link #close()}, so you can put clean up code here.
+ * You don't need to call {@link #close()} after this.
+ *
+ * @param controller the controller for this event
+ */
+ public void onDisconnected(@NonNull MediaController2 controller) { }
+
+ /**
+ * Called when a controller sent a session command.
+ */
+ public void onSessionCommand(@NonNull MediaController2 controller,
+ @NonNull Session2Command command, @Nullable Bundle args) {
+ }
+ }
+}
diff --git a/media/java/android/media/MediaItem2.java b/media/java/android/media/MediaItem2.java
index aa2a937..4e469e3 100644
--- a/media/java/android/media/MediaItem2.java
+++ b/media/java/android/media/MediaItem2.java
@@ -33,25 +33,12 @@
import java.util.concurrent.Executor;
/**
- * A class with information on a single media item with the metadata information. Here are use
- * cases.
- * <ul>
- * <li>Specify media items to {@link SessionPlayer2} for playback.
- * <li>Share media items across the processes.
- * </ul>
- * <p>
- * Subclasses of the session player may only accept certain subclasses of the media items. Check
- * the player documentation that you're interested in.
- * <p>
- * When it's shared across the processes, we cannot guarantee that they contain the right values
- * because media items are application dependent especially for the metadata.
- * <p>
- * This object is thread safe.
+ * A class with information on a single media item with the metadata information.
* <p>
* This API is not generally intended for third party application developers.
* Use the <a href="{@docRoot}tools/extras/support-library.html">Support Library</a>
* {@link androidx.media2.MediaItem} for consistent behavior across all devices.
- * </p>
+ * <p>
* @hide
*/
public class MediaItem2 implements Parcelable {
diff --git a/media/java/android/media/MediaSession2.java b/media/java/android/media/MediaSession2.java
new file mode 100644
index 0000000..222dfc0
--- /dev/null
+++ b/media/java/android/media/MediaSession2.java
@@ -0,0 +1,345 @@
+/*
+ * 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 android.media.MediaConstants.KEY_ALLOWED_COMMANDS;
+import static android.media.MediaConstants.KEY_PACKAGE_NAME;
+import static android.media.MediaConstants.KEY_PID;
+import static android.media.MediaConstants.KEY_SESSION2_STUB;
+import static android.media.Session2Token.TYPE_SESSION;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.media.session.MediaSessionManager;
+import android.media.session.MediaSessionManager.RemoteUserInfo;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Process;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * Allows a media app to expose its transport controls and playback information in a process to
+ * other processes including the Android framework and other apps.
+ * <p>
+ * This API is not generally intended for third party application developers.
+ * Use the <a href="{@docRoot}tools/extras/support-library.html">Support Library</a>
+ * {@link androidx.media2.MediaSession} for consistent behavior across all devices.
+ * @hide
+ */
+public class MediaSession2 implements AutoCloseable {
+ static final String TAG = "MediaSession";
+ static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final Object mLock = new Object();
+ //@GuardedBy("mLock")
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final Map<Controller2Link, ControllerInfo> mConnectedControllers = new HashMap<>();
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final Context mContext;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final Executor mCallbackExecutor;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final SessionCallback mCallback;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final Session2Link mSessionStub;
+
+ private final String mSessionId;
+ private final PendingIntent mSessionActivity;
+ private final Session2Token mSessionToken;
+ private final MediaSessionManager mSessionManager;
+
+ MediaSession2(Context context, String id, PendingIntent sessionActivity,
+ Executor callbackExecutor, SessionCallback callback) {
+ mContext = context;
+ mSessionId = id;
+ mSessionActivity = sessionActivity;
+ mCallbackExecutor = callbackExecutor;
+ mCallback = callback;
+ mSessionStub = new Session2Link(this);
+ mSessionToken = new Session2Token(Process.myUid(), TYPE_SESSION, context.getPackageName(),
+ mSessionStub);
+ mSessionManager = (MediaSessionManager) mContext.getSystemService(
+ Context.MEDIA_SESSION_SERVICE);
+ }
+
+ @Override
+ public void close() throws Exception {
+ // TODO: Implement this
+ }
+
+ // AML method
+ boolean isClosed() {
+ // TODO: Implement this
+ return true;
+ }
+
+ // AML method
+ void onConnect(final Controller2Link controller, int seq, Bundle connectionRequest) {
+ if (controller == null || connectionRequest == null) {
+ return;
+ }
+ final int uid = Binder.getCallingUid();
+ final int callingPid = Binder.getCallingPid();
+ final long token = Binder.clearCallingIdentity();
+ // Binder.getCallingPid() can be 0 for an oneway call from the remote process.
+ // If it's the case, use PID from the ConnectionRequest.
+ final int pid = (callingPid != 0) ? callingPid : connectionRequest.getInt(KEY_PID);
+ final String pkg = connectionRequest.getString(KEY_PACKAGE_NAME);
+ try {
+ RemoteUserInfo remoteUserInfo = new RemoteUserInfo(pkg, pid, uid);
+ final ControllerInfo controllerInfo = new ControllerInfo(remoteUserInfo,
+ mSessionManager.isTrustedForMediaControl(remoteUserInfo), controller);
+ mCallbackExecutor.execute(() -> {
+ if (isClosed()) {
+ return;
+ }
+ controllerInfo.mAllowedCommands =
+ mCallback.onConnect(MediaSession2.this, controllerInfo);
+ // 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 =
+ controllerInfo.mAllowedCommands != null || controllerInfo.isTrusted();
+ if (accept) {
+ if (controllerInfo.mAllowedCommands == null) {
+ // For trusted apps, send non-null allowed commands to keep
+ // connection.
+ controllerInfo.mAllowedCommands = new Session2CommandGroup();
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Accepting connection: " + controllerInfo);
+ }
+ synchronized (mLock) {
+ if (mConnectedControllers.containsKey(controller)) {
+ Log.w(TAG, "Controller " + controllerInfo + " has sent connection"
+ + " request multiple times");
+ }
+ mConnectedControllers.put(controller, controllerInfo);
+ }
+ // If connection is accepted, notify the current state to the controller.
+ // It's needed because we cannot call synchronous calls between
+ // session/controller.
+ Bundle connectionResult = new Bundle();
+ connectionResult.putParcelable(KEY_SESSION2_STUB, mSessionStub);
+ connectionResult.putParcelable(KEY_ALLOWED_COMMANDS,
+ controllerInfo.mAllowedCommands);
+
+ // Double check if session is still there, because close() can be called in
+ // another thread.
+ if (isClosed()) {
+ return;
+ }
+ try {
+ controller.notifyConnected(
+ controllerInfo.mNextSeqNumber++, connectionResult);
+ } catch (RuntimeException e) {
+ // Controller may be died prematurely.
+ }
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "Rejecting connection, controllerInfo=" + controllerInfo);
+ }
+ try {
+ controller.notifyDisconnected(controllerInfo.mNextSeqNumber++);
+ } catch (RuntimeException e) {
+ // Controller may be died prematurely.
+ }
+ }
+ });
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ // AML method
+ void onDisconnect(final Controller2Link controller, int seq) {
+ if (controller == null) {
+ return;
+ }
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ final ControllerInfo controllerInfo = mConnectedControllers.get(controller);
+ mCallbackExecutor.execute(() -> {
+ try {
+ controller.notifyDisconnected(controllerInfo.mNextSeqNumber++);
+ } catch (RuntimeException e) {
+ // Controller may be died prematurely.
+ }
+ });
+ mConnectedControllers.remove(controller);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ // AML method
+ void onSessionCommand(final Controller2Link controller, final int seq,
+ final Session2Command command, final Bundle args) {
+ // TODO: Implement this
+ }
+
+ /**
+ * Information of a controller.
+ * <p>
+ * This API is not generally intended for third party application developers.
+ */
+ public static final class ControllerInfo {
+ private final RemoteUserInfo mRemoteUserInfo;
+ private final boolean mIsTrusted;
+ private final Controller2Link mControllerBinder;
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ int mNextSeqNumber;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ Session2CommandGroup mAllowedCommands;
+
+ /**
+ * @param remoteUserInfo remote user info
+ * @param trusted {@code true} if trusted, {@code false} otherwise
+ * @param controllerBinder Controller2Link. Can be {@code null} only when a
+ * MediaBrowserCompat connects to MediaSessionService and ControllerInfo is
+ * needed for SessionCallback#onConnected().
+ */
+ ControllerInfo(@NonNull RemoteUserInfo remoteUserInfo, boolean trusted,
+ @Nullable Controller2Link controllerBinder) {
+ mRemoteUserInfo = remoteUserInfo;
+ mIsTrusted = trusted;
+ mControllerBinder = controllerBinder;
+ }
+
+ /**
+ * @hide
+ */
+ public @NonNull RemoteUserInfo getRemoteUserInfo() {
+ return mRemoteUserInfo;
+ }
+
+ /**
+ * @return package name of the controller. Can be
+ * {@link androidx.media.MediaSessionManager.RemoteUserInfo#LEGACY_CONTROLLER} if
+ * the package name cannot be obtained.
+ */
+ public @NonNull String getPackageName() {
+ return mRemoteUserInfo.getPackageName();
+ }
+
+ /**
+ * @return uid of the controller. Can be a negative value if the uid cannot be obtained.
+ */
+ public int getUid() {
+ return mRemoteUserInfo.getUid();
+ }
+
+ /**
+ * Return if the controller has granted {@code android.permission.MEDIA_CONTENT_CONTROL} or
+ * has a enabled notification listener so can be trusted to accept connection and incoming
+ * command request.
+ *
+ * @return {@code true} if the controller is trusted.
+ * @hide
+ */
+ public boolean isTrusted() {
+ return mIsTrusted;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mControllerBinder, mRemoteUserInfo);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof ControllerInfo)) {
+ return false;
+ }
+ if (this == obj) {
+ return true;
+ }
+ ControllerInfo other = (ControllerInfo) obj;
+ if (mControllerBinder != null || other.mControllerBinder != null) {
+ return Objects.equals(mControllerBinder, other.mControllerBinder);
+ }
+ return mRemoteUserInfo.equals(other.mRemoteUserInfo);
+ }
+
+ @Override
+ public String toString() {
+ return "ControllerInfo {pkg=" + mRemoteUserInfo.getPackageName() + ", uid="
+ + mRemoteUserInfo.getUid() + ", allowedCommands=" + mAllowedCommands + "})";
+ }
+
+ @Nullable Controller2Link getControllerBinder() {
+ return mControllerBinder;
+ }
+ }
+
+ /**
+ * Callback to be called for all incoming commands from {@link MediaController2}s.
+ * <p>
+ * This API is not generally intended for third party application developers.
+ */
+ public abstract static class SessionCallback {
+ /**
+ * Called when a controller is created for this session. Return allowed commands for
+ * controller. By default it allows all connection requests and commands.
+ * <p>
+ * You can reject the connection by returning {@code null}. In that case, controller
+ * receives {@link MediaController2.ControllerCallback#onDisconnected(MediaController2)}
+ * and cannot be used.
+ *
+ * @param session the session for this event
+ * @param controller controller information.
+ * @return allowed commands. Can be {@code null} to reject connection.
+ */
+ @Nullable public Session2CommandGroup onConnect(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller) {
+ Session2CommandGroup commands = new Session2CommandGroup.Builder()
+ .addAllPredefinedCommands(Session2Command.COMMAND_VERSION_1)
+ .build();
+ return commands;
+ }
+
+ /**
+ * Called when a controller is disconnected
+ *
+ * @param session the session for this event
+ * @param controller controller information
+ */
+ public void onDisconnected(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller) { }
+
+ /**
+ * Called when a controller sent a session command.
+ */
+ public void onSessionCommand(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller, @NonNull Session2Command command,
+ @Nullable Bundle args) {
+ }
+ }
+}
+
diff --git a/media/java/android/media/Session2Command.aidl b/media/java/android/media/Session2Command.aidl
new file mode 100644
index 0000000..43a7b12
--- /dev/null
+++ b/media/java/android/media/Session2Command.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+parcelable Session2Command;
diff --git a/media/java/android/media/Session2Link.java b/media/java/android/media/Session2Link.java
new file mode 100644
index 0000000..29f2685
--- /dev/null
+++ b/media/java/android/media/Session2Link.java
@@ -0,0 +1,171 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.Objects;
+
+/**
+ * Handles incoming commands from {@link MediaController2} and {@link MediaBrowser2}
+ * to both {@link MediaSession2} and {@link MediaLibrarySession}.
+ * @hide
+ */
+// @SystemApi
+public final class Session2Link implements Parcelable {
+ private static final String TAG = "Session2Link";
+ private static final boolean DEBUG = MediaSession2.DEBUG;
+
+ public static final Parcelable.Creator<Session2Link> CREATOR =
+ new Parcelable.Creator<Session2Link>() {
+ @Override
+ public Session2Link createFromParcel(Parcel in) {
+ return new Session2Link(in);
+ }
+
+ @Override
+ public Session2Link[] newArray(int size) {
+ return new Session2Link[size];
+ }
+ };
+
+ private final MediaSession2 mSession;
+ private final IMediaSession2 mISession;
+
+ public Session2Link(MediaSession2 session) {
+ mSession = session;
+ mISession = new Session2Stub();
+ }
+
+ Session2Link(Parcel in) {
+ mSession = null;
+ mISession = IMediaSession2.Stub.asInterface(in.readStrongBinder());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeStrongBinder(mISession.asBinder());
+ }
+
+ @Override
+ public int hashCode() {
+ return mISession.asBinder().hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof Session2Link)) {
+ return false;
+ }
+ Session2Link other = (Session2Link) obj;
+ return Objects.equals(mISession.asBinder(), other.mISession.asBinder());
+ }
+
+ /** Link to death with mISession */
+ public void linkToDeath(@NonNull IBinder.DeathRecipient recipient, int flags) {
+ if (mISession != null) {
+ try {
+ mISession.asBinder().linkToDeath(recipient, flags);
+ } catch (RemoteException e) {
+ if (DEBUG) {
+ Log.d(TAG, "Session died too early.", e);
+ }
+ }
+ }
+ }
+
+ /** Unlink to death with mISession */
+ public boolean unlinkToDeath(@NonNull IBinder.DeathRecipient recipient, int flags) {
+ if (mISession != null) {
+ return mISession.asBinder().unlinkToDeath(recipient, flags);
+ }
+ return true;
+ }
+
+ /** Interface method for IMediaSession2.connect */
+ public void connect(final Controller2Link caller, int seq, Bundle connectionRequest) {
+ try {
+ mISession.connect(caller, seq, connectionRequest);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Interface method for IMediaSession2.disconnect */
+ public void disconnect(final Controller2Link caller, int seq) {
+ try {
+ mISession.disconnect(caller, seq);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Interface method for IMediaSession2.sendSessionCommand */
+ public void sendSessionCommand(final Controller2Link caller, final int seq,
+ final Session2Command command, final Bundle args) {
+ try {
+ mISession.sendSessionCommand(caller, seq, command, args);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Stub implementation for IMediaSession2.connect */
+ public void onConnect(final Controller2Link caller, int seq, Bundle connectionRequest) {
+ mSession.onConnect(caller, seq, connectionRequest);
+ }
+
+ /** Stub implementation for IMediaSession2.disconnect */
+ public void onDisconnect(final Controller2Link caller, int seq) {
+ mSession.onDisconnect(caller, seq);
+ }
+
+ /** Stub implementation for IMediaSession2.sendSessionCommand */
+ public void onSessionCommand(final Controller2Link caller, final int seq,
+ final Session2Command command, final Bundle args) {
+ mSession.onSessionCommand(caller, seq, command, args);
+ }
+
+ private class Session2Stub extends IMediaSession2.Stub {
+ @Override
+ public void connect(final Controller2Link caller, int seq, Bundle connectionRequest) {
+ Session2Link.this.onConnect(caller, seq, connectionRequest);
+ }
+
+ @Override
+ public void disconnect(final Controller2Link caller, int seq) {
+ Session2Link.this.onDisconnect(caller, seq);
+ }
+
+ @Override
+ public void sendSessionCommand(final Controller2Link caller, final int seq,
+ final Session2Command command, final Bundle args) {
+ Session2Link.this.onSessionCommand(caller, seq, command, args);
+ }
+ }
+}
diff --git a/media/java/android/media/Session2Token.java b/media/java/android/media/Session2Token.java
index af14078..4fa31cd 100644
--- a/media/java/android/media/Session2Token.java
+++ b/media/java/android/media/Session2Token.java
@@ -24,7 +24,6 @@
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
-import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
@@ -39,7 +38,7 @@
* If it's representing a session service, it may not be ongoing.
* <p>
* This API is not generally intended for third party application developers.
- * Use the <a href="{@docRoot}jetpack/androidx/">Support Library</a> instead.
+ * Use the <a href="{@docRoot}tools/extras/support-library.html">Support Library</a> instead.
* {@link androidx.media2.SessionToken} for consistent behavior across all devices.
* <p>
* This may be passed to apps by the session owner to allow them to create a
@@ -85,12 +84,12 @@
public static final int TYPE_SESSION = 0;
/**
- * Type for {@link MediaSessionService2}.
+ * Type for {@link MediaSession2Service}.
*/
public static final int TYPE_SESSION_SERVICE = 1;
/**
- * Type for {@link MediaLibraryService2}.
+ * Type for {@link MediaLibrary2Service}.
*/
public static final int TYPE_LIBRARY_SERVICE = 2;
@@ -98,12 +97,11 @@
private final @TokenType int mType;
private final String mPackageName;
private final String mServiceName;
- private final IBinder mISession;
+ private final Session2Link mSessionLink;
private final ComponentName mComponentName;
/**
- * Constructor for the token. You can create token of {@link MediaSessionService2},
- * {@link MediaLibraryService2} for {@link MediaController2} or {@link MediaBrowser2}.
+ * Constructor for the token.
*
* @param context The context.
* @param serviceComponent The component name of the service.
@@ -120,7 +118,7 @@
final int uid = getUid(manager, serviceComponent.getPackageName());
// TODO: Uncomment below to stop hardcode type.
- final int type = TYPE_SESSION_SERVICE;
+ final int type = TYPE_SESSION;
// final int type;
// if (isInterfaceDeclared(manager, MediaLibraryService2.SERVICE_INTERFACE,
// serviceComponent)) {
@@ -141,27 +139,26 @@
mServiceName = serviceComponent.getClassName();
mUid = uid;
mType = type;
- mISession = null;
+ mSessionLink = null;
}
- // TODO: Uncomment below
-// Session2Token(int uid, int type, String packageName, IMediaSession2 iSession) {
-// mUid = uid;
-// mType = type;
-// mPackageName = packageName;
-// mServiceName = null;
-// mComponentName = null;
-// mISession = iSession.asBinder();
-// }
+ Session2Token(int uid, int type, String packageName, Session2Link sessionLink) {
+ mUid = uid;
+ mType = type;
+ mPackageName = packageName;
+ mServiceName = null;
+ mComponentName = null;
+ mSessionLink = sessionLink;
+ }
Session2Token(Parcel in) {
mUid = in.readInt();
mType = in.readInt();
mPackageName = in.readString();
mServiceName = in.readString();
- // TODO: Uncomment below and stop hardcode mISession
- mISession = null;
- //mISession = ISession.Stub.asInterface(in.readStrongBinder());
+ // TODO: Uncomment below and stop hardcode mSessionLink
+ mSessionLink = null;
+ //mSessionLink = ISession.Stub.asInterface(in.readStrongBinder());
mComponentName = ComponentName.unflattenFromString(in.readString());
}
@@ -172,7 +169,7 @@
dest.writeString(mPackageName);
dest.writeString(mServiceName);
// TODO: Uncomment below
- //dest.writeStrongBinder(mISession.asBinder());
+ //dest.writeStrongBinder(mSessionLink.asBinder());
dest.writeString(mComponentName == null ? "" : mComponentName.flattenToString());
}
@@ -183,7 +180,7 @@
@Override
public int hashCode() {
- return Objects.hash(mType, mUid, mPackageName, mServiceName, mISession);
+ return Objects.hash(mType, mUid, mPackageName, mServiceName, mSessionLink);
}
@Override
@@ -196,13 +193,13 @@
&& TextUtils.equals(mPackageName, other.mPackageName)
&& TextUtils.equals(mServiceName, other.mServiceName)
&& mType == other.mType
- && Objects.equals(mISession, other.mISession);
+ && Objects.equals(mSessionLink, other.mSessionLink);
}
@Override
public String toString() {
return "Session2Token {pkg=" + mPackageName + " type=" + mType
- + " service=" + mServiceName + " IMediaSession2=" + mISession + "}";
+ + " service=" + mServiceName + " Session2Link=" + mSessionLink + "}";
}
/**
@@ -249,8 +246,8 @@
/**
* @hide
*/
- public Object getBinder() {
- return mISession;
+ public Session2Link getSessionLink() {
+ return mSessionLink;
}
private static boolean isInterfaceDeclared(PackageManager manager, String serviceInterface,