| /* |
| * 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) {} |
| } |
| } |