/* * 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 com.android.modules.utils.build.SdkLevel; 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 AndroidX * Media2 session * Library for consistent behavior across all devices. *
* 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
* @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
* @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
* Builder for {@link MediaSession2}.
*
* 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.
*
* 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 {
if (SdkLevel.isAtLeastS()) {
MediaCommunicationManager manager =
mContext.getSystemService(MediaCommunicationManager.class);
manager.notifySession2Created(session2.getToken());
} else {
MediaSessionManager manager =
mContext.getSystemService(MediaSessionManager.class);
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 AndroidX
* Media2 session
* Library for consistent behavior across all devices.
*
* 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
* 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}.
*
* You can reject the connection by returning {@code null}. In that case, controller
* receives {@link MediaController2.ControllerCallback#onDisconnected(MediaController2)}
* and cannot be used.
*
* 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.
*
* 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) {}
}
}