blob: 081e76ab0215e9373f900ce5036bbbd743c31d56 [file] [log] [blame]
/*
* 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_CONNECTION_HINTS;
import static android.media.MediaConstants.KEY_PACKAGE_NAME;
import static android.media.MediaConstants.KEY_PID;
import static android.media.MediaConstants.KEY_PLAYBACK_ACTIVE;
import static android.media.MediaConstants.KEY_SESSION2LINK;
import static android.media.MediaConstants.KEY_TOKEN_EXTRAS;
import static android.media.Session2Command.Result.RESULT_ERROR_UNKNOWN_ERROR;
import static android.media.Session2Command.Result.RESULT_INFO_SKIPPED;
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.content.Intent;
import android.media.session.MediaSessionManager;
import android.media.session.MediaSessionManager.RemoteUserInfo;
import android.os.BadParcelableException;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcel;
import android.os.Process;
import android.os.ResultReceiver;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor;
/**
* This API is not generally intended for third party application developers.
* Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
* <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
* Library</a> for consistent behavior across all devices.
* <p>
* 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.
*/
public class MediaSession2 implements AutoCloseable {
static final String TAG = "MediaSession2";
static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
// Note: This checks the uniqueness of a session ID only in a single process.
// When the framework becomes able to check the uniqueness, this logic should be removed.
//@GuardedBy("MediaSession.class")
private static final List<String> SESSION_ID_LIST = new ArrayList<>();
@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;
private final Handler mResultHandler;
//@GuardedBy("mLock")
private boolean mClosed;
//@GuardedBy("mLock")
private boolean mPlaybackActive;
//@GuardedBy("mLock")
private ForegroundServiceEventCallback mForegroundServiceEventCallback;
MediaSession2(@NonNull Context context, @NonNull String id, PendingIntent sessionActivity,
@NonNull Executor callbackExecutor, @NonNull SessionCallback callback,
@NonNull Bundle tokenExtras) {
synchronized (MediaSession2.class) {
if (SESSION_ID_LIST.contains(id)) {
throw new IllegalStateException("Session ID must be unique. ID=" + id);
}
SESSION_ID_LIST.add(id);
}
mContext = context;
mSessionId = id;
mSessionActivity = sessionActivity;
mCallbackExecutor = callbackExecutor;
mCallback = callback;
mSessionStub = new Session2Link(this);
mSessionToken = new Session2Token(Process.myUid(), TYPE_SESSION, context.getPackageName(),
mSessionStub, tokenExtras);
mSessionManager = (MediaSessionManager) mContext.getSystemService(
Context.MEDIA_SESSION_SERVICE);
// NOTE: mResultHandler uses main looper, so this MUST NOT be blocked.
mResultHandler = new Handler(context.getMainLooper());
mClosed = false;
}
@Override
public void close() {
try {
List<ControllerInfo> controllerInfos;
ForegroundServiceEventCallback callback;
synchronized (mLock) {
if (mClosed) {
return;
}
mClosed = true;
controllerInfos = getConnectedControllers();
mConnectedControllers.clear();
callback = mForegroundServiceEventCallback;
mForegroundServiceEventCallback = null;
}
synchronized (MediaSession2.class) {
SESSION_ID_LIST.remove(mSessionId);
}
if (callback != null) {
callback.onSessionClosed(this);
}
for (ControllerInfo info : controllerInfos) {
info.notifyDisconnected();
}
} catch (Exception e) {
// Should not be here.
}
}
/**
* Returns the session ID
*/
@NonNull
public String getId() {
return mSessionId;
}
/**
* Returns the {@link Session2Token} for creating {@link MediaController2}.
*/
@NonNull
public Session2Token getToken() {
return mSessionToken;
}
/**
* Broadcasts a session command to all the connected controllers
* <p>
* @param command the session command
* @param args optional arguments
*/
public void broadcastSessionCommand(@NonNull Session2Command command, @Nullable Bundle args) {
if (command == null) {
throw new IllegalArgumentException("command shouldn't be null");
}
List<ControllerInfo> controllerInfos = getConnectedControllers();
for (ControllerInfo controller : controllerInfos) {
controller.sendSessionCommand(command, args, null);
}
}
/**
* Sends a session command to a specific controller
* <p>
* @param controller the controller to get the session command
* @param command the session command
* @param args optional arguments
* @return a token which will be sent together in {@link SessionCallback#onCommandResult}
* when its result is received.
*/
@NonNull
public Object sendSessionCommand(@NonNull ControllerInfo controller,
@NonNull Session2Command command, @Nullable Bundle args) {
if (controller == null) {
throw new IllegalArgumentException("controller shouldn't be null");
}
if (command == null) {
throw new IllegalArgumentException("command shouldn't be null");
}
ResultReceiver resultReceiver = new ResultReceiver(mResultHandler) {
protected void onReceiveResult(int resultCode, Bundle resultData) {
controller.receiveCommandResult(this);
mCallbackExecutor.execute(() -> {
mCallback.onCommandResult(MediaSession2.this, controller, this,
command, new Session2Command.Result(resultCode, resultData));
});
}
};
controller.sendSessionCommand(command, args, resultReceiver);
return resultReceiver;
}
/**
* Cancels the session command previously sent.
*
* @param controller the controller to get the session command
* @param token the token which is returned from {@link #sendSessionCommand}.
*/
public void cancelSessionCommand(@NonNull ControllerInfo controller, @NonNull Object token) {
if (controller == null) {
throw new IllegalArgumentException("controller shouldn't be null");
}
if (token == null) {
throw new IllegalArgumentException("token shouldn't be null");
}
controller.cancelSessionCommand(token);
}
/**
* Sets whether the playback is active (i.e. playing something)
*
* @param playbackActive {@code true} if the playback active, {@code false} otherwise.
**/
public void setPlaybackActive(boolean playbackActive) {
final ForegroundServiceEventCallback serviceCallback;
synchronized (mLock) {
if (mPlaybackActive == playbackActive) {
return;
}
mPlaybackActive = playbackActive;
serviceCallback = mForegroundServiceEventCallback;
}
if (serviceCallback != null) {
serviceCallback.onPlaybackActiveChanged(this, playbackActive);
}
List<ControllerInfo> controllerInfos = getConnectedControllers();
for (ControllerInfo controller : controllerInfos) {
controller.notifyPlaybackActiveChanged(playbackActive);
}
}
/**
* Returns whehther the playback is active (i.e. playing something)
*
* @return {@code true} if the playback active, {@code false} otherwise.
*/
public boolean isPlaybackActive() {
synchronized (mLock) {
return mPlaybackActive;
}
}
/**
* Gets the list of the connected controllers
*
* @return list of the connected controllers.
*/
@NonNull
public List<ControllerInfo> getConnectedControllers() {
List<ControllerInfo> controllers = new ArrayList<>();
synchronized (mLock) {
controllers.addAll(mConnectedControllers.values());
}
return controllers;
}
/**
* Returns whether the given bundle includes non-framework Parcelables.
*/
static boolean hasCustomParcelable(@Nullable Bundle bundle) {
if (bundle == null) {
return false;
}
// Try writing the bundle to parcel, and read it with framework classloader.
Parcel parcel = null;
try {
parcel = Parcel.obtain();
parcel.writeBundle(bundle);
parcel.setDataPosition(0);
Bundle out = parcel.readBundle(null);
// Calling Bundle#size() will trigger Bundle#unparcel().
out.size();
} catch (BadParcelableException e) {
Log.d(TAG, "Custom parcelable in bundle.", e);
return true;
} finally {
if (parcel != null) {
parcel.recycle();
}
}
return false;
}
boolean isClosed() {
synchronized (mLock) {
return mClosed;
}
}
SessionCallback getCallback() {
return mCallback;
}
void setForegroundServiceEventCallback(ForegroundServiceEventCallback callback) {
synchronized (mLock) {
if (mForegroundServiceEventCallback == callback) {
return;
}
if (mForegroundServiceEventCallback != null && callback != null) {
throw new IllegalStateException("A session cannot be added to multiple services");
}
mForegroundServiceEventCallback = callback;
}
}
// Called by Session2Link.onConnect and MediaSession2Service.MediaSession2ServiceStub.connect
void onConnect(final Controller2Link controller, int callingPid, int callingUid, int seq,
Bundle connectionRequest) {
if (callingPid == 0) {
// The pid here is from Binder.getCallingPid(), which can be 0 for an oneway call from
// the remote process. If it's the case, use PID from the connectionRequest.
callingPid = connectionRequest.getInt(KEY_PID);
}
String callingPkg = connectionRequest.getString(KEY_PACKAGE_NAME);
RemoteUserInfo remoteUserInfo = new RemoteUserInfo(callingPkg, callingPid, callingUid);
Bundle connectionHints = connectionRequest.getBundle(KEY_CONNECTION_HINTS);
if (connectionHints == null) {
Log.w(TAG, "connectionHints shouldn't be null.");
connectionHints = Bundle.EMPTY;
} else if (hasCustomParcelable(connectionHints)) {
Log.w(TAG, "connectionHints contain custom parcelable. Ignoring.");
connectionHints = Bundle.EMPTY;
}
final ControllerInfo controllerInfo = new ControllerInfo(
remoteUserInfo,
mSessionManager.isTrustedForMediaControl(remoteUserInfo),
controller,
connectionHints);
mCallbackExecutor.execute(() -> {
boolean connected = false;
try {
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.
if (controllerInfo.mAllowedCommands == null && !controllerInfo.isTrusted()) {
return;
}
if (controllerInfo.mAllowedCommands == null) {
// For trusted apps, send non-null allowed commands to keep
// connection.
controllerInfo.mAllowedCommands =
new Session2CommandGroup.Builder().build();
}
if (DEBUG) {
Log.d(TAG, "Accepting connection: " + 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_SESSION2LINK, mSessionStub);
connectionResult.putParcelable(KEY_ALLOWED_COMMANDS,
controllerInfo.mAllowedCommands);
connectionResult.putBoolean(KEY_PLAYBACK_ACTIVE, isPlaybackActive());
connectionResult.putBundle(KEY_TOKEN_EXTRAS, mSessionToken.getExtras());
// Double check if session is still there, because close() can be called in
// another thread.
if (isClosed()) {
return;
}
controllerInfo.notifyConnected(connectionResult);
synchronized (mLock) {
if (mConnectedControllers.containsKey(controller)) {
Log.w(TAG, "Controller " + controllerInfo + " has sent connection"
+ " request multiple times");
}
mConnectedControllers.put(controller, controllerInfo);
}
mCallback.onPostConnect(MediaSession2.this, controllerInfo);
connected = true;
} finally {
if (!connected) {
if (DEBUG) {
Log.d(TAG, "Rejecting connection or notifying that session is closed"
+ ", controllerInfo=" + controllerInfo);
}
synchronized (mLock) {
mConnectedControllers.remove(controller);
}
controllerInfo.notifyDisconnected();
}
}
});
}
// Called by Session2Link.onDisconnect
void onDisconnect(@NonNull final Controller2Link controller, int seq) {
final ControllerInfo controllerInfo;
synchronized (mLock) {
controllerInfo = mConnectedControllers.remove(controller);
}
if (controllerInfo == null) {
return;
}
mCallbackExecutor.execute(() -> {
mCallback.onDisconnected(MediaSession2.this, controllerInfo);
});
}
// Called by Session2Link.onSessionCommand
void onSessionCommand(@NonNull final Controller2Link controller, final int seq,
final Session2Command command, final Bundle args,
@Nullable ResultReceiver resultReceiver) {
if (controller == null) {
return;
}
final ControllerInfo controllerInfo;
synchronized (mLock) {
controllerInfo = mConnectedControllers.get(controller);
}
if (controllerInfo == null) {
return;
}
// TODO: check allowed commands.
synchronized (mLock) {
controllerInfo.addRequestedCommandSeqNumber(seq);
}
mCallbackExecutor.execute(() -> {
if (!controllerInfo.removeRequestedCommandSeqNumber(seq)) {
resultReceiver.send(RESULT_INFO_SKIPPED, null);
return;
}
Session2Command.Result result = mCallback.onSessionCommand(
MediaSession2.this, controllerInfo, command, args);
if (resultReceiver != null) {
if (result == null) {
resultReceiver.send(RESULT_INFO_SKIPPED, null);
} else {
resultReceiver.send(result.getResultCode(), result.getResultData());
}
}
});
}
// Called by Session2Link.onCancelCommand
void onCancelCommand(@NonNull final Controller2Link controller, final int seq) {
final ControllerInfo controllerInfo;
synchronized (mLock) {
controllerInfo = mConnectedControllers.get(controller);
}
if (controllerInfo == null) {
return;
}
controllerInfo.removeRequestedCommandSeqNumber(seq);
}
/**
* This API is not generally intended for third party application developers.
* Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
* <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
* Library</a> for consistent behavior across all devices.
* <p>
* Builder for {@link MediaSession2}.
* <p>
* Any incoming event from the {@link MediaController2} will be handled on the callback
* executor. If it's not set, {@link Context#getMainExecutor()} will be used by default.
*/
public static final class Builder {
private Context mContext;
private String mId;
private PendingIntent mSessionActivity;
private Executor mCallbackExecutor;
private SessionCallback mCallback;
private Bundle mExtras;
/**
* Creates a builder for {@link MediaSession2}.
*
* @param context Context
* @throws IllegalArgumentException if context is {@code null}.
*/
public Builder(@NonNull Context context) {
if (context == null) {
throw new IllegalArgumentException("context shouldn't be null");
}
mContext = context;
}
/**
* Set an intent for launching UI for this Session. This can be used as a
* quick link to an ongoing media screen. The intent should be for an
* activity that may be started using {@link Context#startActivity(Intent)}.
*
* @param pi The intent to launch to show UI for this session.
* @return The Builder to allow chaining
*/
@NonNull
public Builder setSessionActivity(@Nullable PendingIntent pi) {
mSessionActivity = pi;
return this;
}
/**
* Set ID of the session. If it's not set, an empty string will be used to create a session.
* <p>
* Use this if and only if your app supports multiple playback at the same time and also
* wants to provide external apps to have finer controls of them.
*
* @param id id of the session. Must be unique per package.
* @throws IllegalArgumentException if id is {@code null}.
* @return The Builder to allow chaining
*/
@NonNull
public Builder setId(@NonNull String id) {
if (id == null) {
throw new IllegalArgumentException("id shouldn't be null");
}
mId = id;
return this;
}
/**
* Set callback for the session and its executor.
*
* @param executor callback executor
* @param callback session callback.
* @return The Builder to allow chaining
*/
@NonNull
public Builder setSessionCallback(@NonNull Executor executor,
@NonNull SessionCallback callback) {
mCallbackExecutor = executor;
mCallback = callback;
return this;
}
/**
* Set extras for the session token. If null or not set, {@link Session2Token#getExtras()}
* will return an empty {@link Bundle}. An {@link IllegalArgumentException} will be thrown
* if the bundle contains any non-framework Parcelable objects.
*
* @return The Builder to allow chaining
* @see Session2Token#getExtras()
*/
@NonNull
public Builder setExtras(@NonNull Bundle extras) {
if (extras == null) {
throw new NullPointerException("extras shouldn't be null");
}
if (hasCustomParcelable(extras)) {
throw new IllegalArgumentException(
"extras shouldn't contain any custom parcelables");
}
mExtras = new Bundle(extras);
return this;
}
/**
* Build {@link MediaSession2}.
*
* @return a new session
* @throws IllegalStateException if the session with the same id is already exists for the
* package.
*/
@NonNull
public MediaSession2 build() {
if (mCallbackExecutor == null) {
mCallbackExecutor = mContext.getMainExecutor();
}
if (mCallback == null) {
mCallback = new SessionCallback() {};
}
if (mId == null) {
mId = "";
}
if (mExtras == null) {
mExtras = Bundle.EMPTY;
}
MediaSession2 session2 = new MediaSession2(mContext, mId, mSessionActivity,
mCallbackExecutor, mCallback, mExtras);
// Notify framework about the newly create session after the constructor is finished.
// Otherwise, framework may access the session before the initialization is finished.
try {
MediaSessionManager manager = (MediaSessionManager) mContext.getSystemService(
Context.MEDIA_SESSION_SERVICE);
manager.notifySession2Created(session2.getToken());
} catch (Exception e) {
session2.close();
throw e;
}
return session2;
}
}
/**
* This API is not generally intended for third party application developers.
* Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
* <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
* Library</a> for consistent behavior across all devices.
* <p>
* Information of a controller.
*/
public static final class ControllerInfo {
private final RemoteUserInfo mRemoteUserInfo;
private final boolean mIsTrusted;
private final Controller2Link mControllerBinder;
private final Bundle mConnectionHints;
private final Object mLock = new Object();
//@GuardedBy("mLock")
private int mNextSeqNumber;
//@GuardedBy("mLock")
private ArrayMap<ResultReceiver, Integer> mPendingCommands;
//@GuardedBy("mLock")
private ArraySet<Integer> mRequestedCommandSeqNumbers;
@SuppressWarnings("WeakerAccess") /* synthetic access */
Session2CommandGroup mAllowedCommands;
/**
* @param remoteUserInfo remote user info
* @param trusted {@code true} if trusted, {@code false} otherwise
* @param controllerBinder Controller2Link for the connected controller.
* @param connectionHints a session-specific argument sent from the controller for the
* connection. The contents of this bundle may affect the
* connection result.
*/
ControllerInfo(@NonNull RemoteUserInfo remoteUserInfo, boolean trusted,
@Nullable Controller2Link controllerBinder, @NonNull Bundle connectionHints) {
mRemoteUserInfo = remoteUserInfo;
mIsTrusted = trusted;
mControllerBinder = controllerBinder;
mConnectionHints = connectionHints;
mPendingCommands = new ArrayMap<>();
mRequestedCommandSeqNumbers = new ArraySet<>();
}
/**
* @return remote user info of the controller.
*/
@NonNull
public RemoteUserInfo getRemoteUserInfo() {
return mRemoteUserInfo;
}
/**
* @return package name of the controller.
*/
@NonNull
public 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 connection hints sent from controller.
*/
@NonNull
public Bundle getConnectionHints() {
return new Bundle(mConnectionHints);
}
/**
* 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(@Nullable 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
@NonNull
public String toString() {
return "ControllerInfo {pkg=" + mRemoteUserInfo.getPackageName() + ", uid="
+ mRemoteUserInfo.getUid() + ", allowedCommands=" + mAllowedCommands + "})";
}
void notifyConnected(Bundle connectionResult) {
if (mControllerBinder == null) return;
try {
mControllerBinder.notifyConnected(getNextSeqNumber(), connectionResult);
} catch (RuntimeException e) {
// Controller may be died prematurely.
}
}
void notifyDisconnected() {
if (mControllerBinder == null) return;
try {
mControllerBinder.notifyDisconnected(getNextSeqNumber());
} catch (RuntimeException e) {
// Controller may be died prematurely.
}
}
void notifyPlaybackActiveChanged(boolean playbackActive) {
if (mControllerBinder == null) return;
try {
mControllerBinder.notifyPlaybackActiveChanged(getNextSeqNumber(), playbackActive);
} catch (RuntimeException e) {
// Controller may be died prematurely.
}
}
void sendSessionCommand(Session2Command command, Bundle args,
ResultReceiver resultReceiver) {
if (mControllerBinder == null) return;
try {
int seq = getNextSeqNumber();
synchronized (mLock) {
mPendingCommands.put(resultReceiver, seq);
}
mControllerBinder.sendSessionCommand(seq, command, args, resultReceiver);
} catch (RuntimeException e) {
// Controller may be died prematurely.
synchronized (mLock) {
mPendingCommands.remove(resultReceiver);
}
resultReceiver.send(RESULT_ERROR_UNKNOWN_ERROR, null);
}
}
void cancelSessionCommand(@NonNull Object token) {
if (mControllerBinder == null) return;
Integer seq;
synchronized (mLock) {
seq = mPendingCommands.remove(token);
}
if (seq != null) {
mControllerBinder.cancelSessionCommand(seq);
}
}
void receiveCommandResult(ResultReceiver resultReceiver) {
synchronized (mLock) {
mPendingCommands.remove(resultReceiver);
}
}
void addRequestedCommandSeqNumber(int seq) {
synchronized (mLock) {
mRequestedCommandSeqNumbers.add(seq);
}
}
boolean removeRequestedCommandSeqNumber(int seq) {
synchronized (mLock) {
return mRequestedCommandSeqNumbers.remove(seq);
}
}
private int getNextSeqNumber() {
synchronized (mLock) {
return mNextSeqNumber++;
}
}
}
/**
* This API is not generally intended for third party application developers.
* Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
* <a href="{@docRoot}reference/androidx/media2/session/package-summary.html">Media2 session
* Library</a> for consistent behavior across all devices.
* <p>
* Callback to be called for all incoming commands from {@link MediaController2}s.
*/
public abstract static class SessionCallback {
/**
* Called when a controller is created for this session. Return allowed commands for
* controller. By default it returns {@code null}.
* <p>
* You can reject the connection by returning {@code null}. In that case, controller
* receives {@link MediaController2.ControllerCallback#onDisconnected(MediaController2)}
* and cannot be used.
* <p>
* The controller hasn't connected yet in this method, so calls to the controller
* (e.g. {@link #sendSessionCommand}) would be ignored. Override {@link #onPostConnect} for
* the custom initialization for the controller instead.
*
* @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) {
return null;
}
/**
* Called immediately after a controller is connected. This is a convenient method to add
* custom initialization between the session and a controller.
* <p>
* Note that calls to the controller (e.g. {@link #sendSessionCommand}) work here but don't
* work in {@link #onConnect} because the controller hasn't connected yet in
* {@link #onConnect}.
*
* @param session the session for this event
* @param controller controller information.
*/
public void onPostConnect(@NonNull MediaSession2 session,
@NonNull ControllerInfo controller) {
}
/**
* 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.
*
* @param session the session for this event
* @param controller controller information
* @param command the session command
* @param args optional arguments
* @return the result for the session command. If {@code null}, RESULT_INFO_SKIPPED
* will be sent to the session.
*/
@Nullable
public Session2Command.Result onSessionCommand(@NonNull MediaSession2 session,
@NonNull ControllerInfo controller, @NonNull Session2Command command,
@Nullable Bundle args) {
return null;
}
/**
* Called when the command sent to the controller is finished.
*
* @param session the session for this event
* @param controller controller information
* @param token the token got from {@link MediaSession2#sendSessionCommand}
* @param command the session command
* @param result the result of the session command
*/
public void onCommandResult(@NonNull MediaSession2 session,
@NonNull ControllerInfo controller, @NonNull Object token,
@NonNull Session2Command command, @NonNull Session2Command.Result result) {}
}
abstract static class ForegroundServiceEventCallback {
public void onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive) {}
public void onSessionClosed(MediaSession2 session) {}
}
}