638 lines
25 KiB
Java
638 lines
25 KiB
Java
![]() |
/*
|
||
|
* 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.content.ComponentName;
|
||
|
import android.content.Context;
|
||
|
import android.content.Intent;
|
||
|
import android.content.ServiceConnection;
|
||
|
import android.os.Bundle;
|
||
|
import android.os.Handler;
|
||
|
import android.os.IBinder;
|
||
|
import android.os.Process;
|
||
|
import android.os.RemoteException;
|
||
|
import android.os.ResultReceiver;
|
||
|
import android.util.ArrayMap;
|
||
|
import android.util.ArraySet;
|
||
|
import android.util.Log;
|
||
|
|
||
|
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.
|
||
|
*
|
||
|
* 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.
|
||
|
*/
|
||
|
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 Handler mResultHandler;
|
||
|
private final SessionServiceConnection mServiceConnection;
|
||
|
|
||
|
private final Object mLock = new Object();
|
||
|
//@GuardedBy("mLock")
|
||
|
private boolean mClosed;
|
||
|
//@GuardedBy("mLock")
|
||
|
private int mNextSeqNumber;
|
||
|
//@GuardedBy("mLock")
|
||
|
private Session2Link mSessionBinder;
|
||
|
//@GuardedBy("mLock")
|
||
|
private Session2CommandGroup mAllowedCommands;
|
||
|
//@GuardedBy("mLock")
|
||
|
private Session2Token mConnectedToken;
|
||
|
//@GuardedBy("mLock")
|
||
|
private ArrayMap<ResultReceiver, Integer> mPendingCommands;
|
||
|
//@GuardedBy("mLock")
|
||
|
private ArraySet<Integer> mRequestedCommandSeqNumbers;
|
||
|
//@GuardedBy("mLock")
|
||
|
private boolean mPlaybackActive;
|
||
|
|
||
|
/**
|
||
|
* 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 connectionHints a session-specific argument to send to the session when connecting.
|
||
|
* The contents of this bundle may affect the connection result.
|
||
|
* @param executor executor to run callbacks on.
|
||
|
* @param callback controller callback to receive changes in.
|
||
|
*/
|
||
|
MediaController2(@NonNull Context context, @NonNull Session2Token token,
|
||
|
@NonNull Bundle connectionHints, @NonNull Executor executor,
|
||
|
@NonNull ControllerCallback callback) {
|
||
|
if (context == null) {
|
||
|
throw new IllegalArgumentException("context shouldn't be null");
|
||
|
}
|
||
|
if (token == null) {
|
||
|
throw new IllegalArgumentException("token shouldn't be null");
|
||
|
}
|
||
|
mContext = context;
|
||
|
mSessionToken = token;
|
||
|
mCallbackExecutor = (executor == null) ? context.getMainExecutor() : executor;
|
||
|
mCallback = (callback == null) ? new ControllerCallback() {} : callback;
|
||
|
mControllerStub = new Controller2Link(this);
|
||
|
// NOTE: mResultHandler uses main looper, so this MUST NOT be blocked.
|
||
|
mResultHandler = new Handler(context.getMainLooper());
|
||
|
|
||
|
mNextSeqNumber = 0;
|
||
|
mPendingCommands = new ArrayMap<>();
|
||
|
mRequestedCommandSeqNumbers = new ArraySet<>();
|
||
|
|
||
|
boolean connectRequested;
|
||
|
if (token.getType() == TYPE_SESSION) {
|
||
|
mServiceConnection = null;
|
||
|
connectRequested = requestConnectToSession(connectionHints);
|
||
|
} else {
|
||
|
mServiceConnection = new SessionServiceConnection(connectionHints);
|
||
|
connectRequested = requestConnectToService();
|
||
|
}
|
||
|
if (!connectRequested) {
|
||
|
close();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void close() {
|
||
|
synchronized (mLock) {
|
||
|
if (mClosed) {
|
||
|
// Already closed. Ignore rest of clean up code.
|
||
|
// Note: unbindService() throws IllegalArgumentException when it's called twice.
|
||
|
return;
|
||
|
}
|
||
|
if (DEBUG) {
|
||
|
Log.d(TAG, "closing " + this);
|
||
|
}
|
||
|
mClosed = true;
|
||
|
if (mServiceConnection != null) {
|
||
|
// Note: This should be called even when the bindService() has returned false.
|
||
|
mContext.unbindService(mServiceConnection);
|
||
|
}
|
||
|
if (mSessionBinder != null) {
|
||
|
try {
|
||
|
mSessionBinder.disconnect(mControllerStub, getNextSeqNumber());
|
||
|
mSessionBinder.unlinkToDeath(mDeathRecipient, 0);
|
||
|
} catch (RuntimeException e) {
|
||
|
// No-op
|
||
|
}
|
||
|
}
|
||
|
mConnectedToken = null;
|
||
|
mPendingCommands.clear();
|
||
|
mRequestedCommandSeqNumbers.clear();
|
||
|
mCallbackExecutor.execute(() -> {
|
||
|
mCallback.onDisconnected(MediaController2.this);
|
||
|
});
|
||
|
mSessionBinder = null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns {@link Session2Token} of the connected session.
|
||
|
* If it is not connected yet, it returns {@code null}.
|
||
|
* <p>
|
||
|
* This may differ with the {@link Session2Token} from the constructor. For example, if the
|
||
|
* controller is created with the token for {@link MediaSession2Service}, this would return
|
||
|
* token for the {@link MediaSession2} in the service.
|
||
|
*
|
||
|
* @return Session2Token of the connected session, or {@code null} if not connected
|
||
|
*/
|
||
|
@Nullable
|
||
|
public Session2Token getConnectedToken() {
|
||
|
synchronized (mLock) {
|
||
|
return mConnectedToken;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns whether the session's playback is active.
|
||
|
*
|
||
|
* @return {@code true} if playback active. {@code false} otherwise.
|
||
|
* @see ControllerCallback#onPlaybackActiveChanged(MediaController2, boolean)
|
||
|
*/
|
||
|
public boolean isPlaybackActive() {
|
||
|
synchronized (mLock) {
|
||
|
return mPlaybackActive;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sends a session command to the session
|
||
|
* <p>
|
||
|
* @param command the session command
|
||
|
* @param args optional arguments
|
||
|
* @return a token which will be sent together in {@link ControllerCallback#onCommandResult}
|
||
|
* when its result is received.
|
||
|
*/
|
||
|
@NonNull
|
||
|
public Object sendSessionCommand(@NonNull Session2Command command, @Nullable Bundle args) {
|
||
|
if (command == null) {
|
||
|
throw new IllegalArgumentException("command shouldn't be null");
|
||
|
}
|
||
|
|
||
|
ResultReceiver resultReceiver = new ResultReceiver(mResultHandler) {
|
||
|
protected void onReceiveResult(int resultCode, Bundle resultData) {
|
||
|
synchronized (mLock) {
|
||
|
mPendingCommands.remove(this);
|
||
|
}
|
||
|
mCallbackExecutor.execute(() -> {
|
||
|
mCallback.onCommandResult(MediaController2.this, this,
|
||
|
command, new Session2Command.Result(resultCode, resultData));
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
|
||
|
synchronized (mLock) {
|
||
|
if (mSessionBinder != null) {
|
||
|
int seq = getNextSeqNumber();
|
||
|
mPendingCommands.put(resultReceiver, seq);
|
||
|
try {
|
||
|
mSessionBinder.sendSessionCommand(mControllerStub, seq, command, args,
|
||
|
resultReceiver);
|
||
|
} catch (RuntimeException e) {
|
||
|
mPendingCommands.remove(resultReceiver);
|
||
|
resultReceiver.send(RESULT_ERROR_UNKNOWN_ERROR, null);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return resultReceiver;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Cancels the session command previously sent.
|
||
|
*
|
||
|
* @param token the token which is returned from {@link #sendSessionCommand}.
|
||
|
*/
|
||
|
public void cancelSessionCommand(@NonNull Object token) {
|
||
|
if (token == null) {
|
||
|
throw new IllegalArgumentException("token shouldn't be null");
|
||
|
}
|
||
|
synchronized (mLock) {
|
||
|
if (mSessionBinder == null) return;
|
||
|
Integer seq = mPendingCommands.remove(token);
|
||
|
if (seq != null) {
|
||
|
mSessionBinder.cancelSessionCommand(mControllerStub, seq);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Called by Controller2Link.onConnected
|
||
|
void onConnected(int seq, Bundle connectionResult) {
|
||
|
Session2Link sessionBinder = connectionResult.getParcelable(KEY_SESSION2LINK);
|
||
|
Session2CommandGroup allowedCommands =
|
||
|
connectionResult.getParcelable(KEY_ALLOWED_COMMANDS);
|
||
|
boolean playbackActive = connectionResult.getBoolean(KEY_PLAYBACK_ACTIVE);
|
||
|
|
||
|
Bundle tokenExtras = connectionResult.getBundle(KEY_TOKEN_EXTRAS);
|
||
|
if (tokenExtras == null) {
|
||
|
Log.w(TAG, "extras shouldn't be null.");
|
||
|
tokenExtras = Bundle.EMPTY;
|
||
|
} else if (MediaSession2.hasCustomParcelable(tokenExtras)) {
|
||
|
Log.w(TAG, "extras contain custom parcelable. Ignoring.");
|
||
|
tokenExtras = Bundle.EMPTY;
|
||
|
}
|
||
|
|
||
|
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;
|
||
|
mPlaybackActive = playbackActive;
|
||
|
|
||
|
// 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, tokenExtras);
|
||
|
}
|
||
|
mCallbackExecutor.execute(() -> {
|
||
|
mCallback.onConnected(MediaController2.this, allowedCommands);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Called by Controller2Link.onDisconnected
|
||
|
void onDisconnected(int seq) {
|
||
|
// close() will call mCallback.onDisconnected
|
||
|
close();
|
||
|
}
|
||
|
|
||
|
// Called by Controller2Link.onPlaybackActiveChanged
|
||
|
void onPlaybackActiveChanged(int seq, boolean playbackActive) {
|
||
|
synchronized (mLock) {
|
||
|
mPlaybackActive = playbackActive;
|
||
|
}
|
||
|
mCallbackExecutor.execute(() -> {
|
||
|
mCallback.onPlaybackActiveChanged(MediaController2.this, playbackActive);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Called by Controller2Link.onSessionCommand
|
||
|
void onSessionCommand(int seq, Session2Command command, Bundle args,
|
||
|
@Nullable ResultReceiver resultReceiver) {
|
||
|
synchronized (mLock) {
|
||
|
mRequestedCommandSeqNumbers.add(seq);
|
||
|
}
|
||
|
mCallbackExecutor.execute(() -> {
|
||
|
boolean isCanceled;
|
||
|
synchronized (mLock) {
|
||
|
isCanceled = !mRequestedCommandSeqNumbers.remove(seq);
|
||
|
}
|
||
|
if (isCanceled) {
|
||
|
if (resultReceiver != null) {
|
||
|
resultReceiver.send(RESULT_INFO_SKIPPED, null);
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
Session2Command.Result result = mCallback.onSessionCommand(
|
||
|
MediaController2.this, command, args);
|
||
|
if (resultReceiver != null) {
|
||
|
if (result == null) {
|
||
|
resultReceiver.send(RESULT_INFO_SKIPPED, null);
|
||
|
} else {
|
||
|
resultReceiver.send(result.getResultCode(), result.getResultData());
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Called by Controller2Link.onSessionCommand
|
||
|
void onCancelCommand(int seq) {
|
||
|
synchronized (mLock) {
|
||
|
mRequestedCommandSeqNumbers.remove(seq);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private int getNextSeqNumber() {
|
||
|
synchronized (mLock) {
|
||
|
return mNextSeqNumber++;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private Bundle createConnectionRequest(@NonNull Bundle connectionHints) {
|
||
|
Bundle connectionRequest = new Bundle();
|
||
|
connectionRequest.putString(KEY_PACKAGE_NAME, mContext.getPackageName());
|
||
|
connectionRequest.putInt(KEY_PID, Process.myPid());
|
||
|
connectionRequest.putBundle(KEY_CONNECTION_HINTS, connectionHints);
|
||
|
return connectionRequest;
|
||
|
}
|
||
|
|
||
|
private boolean requestConnectToSession(@NonNull Bundle connectionHints) {
|
||
|
Session2Link sessionBinder = mSessionToken.getSessionLink();
|
||
|
Bundle connectionRequest = createConnectionRequest(connectionHints);
|
||
|
try {
|
||
|
sessionBinder.connect(mControllerStub, getNextSeqNumber(), connectionRequest);
|
||
|
} catch (RuntimeException e) {
|
||
|
Log.w(TAG, "Failed to call connection request", e);
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
private boolean requestConnectToService() {
|
||
|
// Service. Needs to get fresh binder whenever connection is needed.
|
||
|
final Intent intent = new Intent(MediaSession2Service.SERVICE_INTERFACE);
|
||
|
intent.setClassName(mSessionToken.getPackageName(), mSessionToken.getServiceName());
|
||
|
|
||
|
// Use bindService() instead of startForegroundService() to start session service for three
|
||
|
// reasons.
|
||
|
// 1. Prevent session service owner's stopSelf() from destroying service.
|
||
|
// With the startForegroundService(), service's call of stopSelf() will trigger immediate
|
||
|
// onDestroy() calls on the main thread even when onConnect() is running in another
|
||
|
// thread.
|
||
|
// 2. Minimize APIs for developers to take care about.
|
||
|
// With bindService(), developers only need to take care about Service.onBind()
|
||
|
// but Service.onStartCommand() should be also taken care about with the
|
||
|
// startForegroundService().
|
||
|
// 3. Future support for UI-less playback
|
||
|
// If a service wants to keep running, it should be either foreground service or
|
||
|
// bound service. But there had been request for the feature for system apps
|
||
|
// and using bindService() will be better fit with it.
|
||
|
synchronized (mLock) {
|
||
|
boolean result = mContext.bindService(
|
||
|
intent, mServiceConnection, Context.BIND_AUTO_CREATE);
|
||
|
if (!result) {
|
||
|
Log.w(TAG, "bind to " + mSessionToken + " failed");
|
||
|
return false;
|
||
|
} else if (DEBUG) {
|
||
|
Log.d(TAG, "bind to " + mSessionToken + " succeeded");
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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 MediaController2}.
|
||
|
* <p>
|
||
|
* Any incoming event from the {@link MediaSession2} 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 Session2Token mToken;
|
||
|
private Bundle mConnectionHints;
|
||
|
private Executor mCallbackExecutor;
|
||
|
private ControllerCallback mCallback;
|
||
|
|
||
|
/**
|
||
|
* Creates a builder for {@link MediaController2}.
|
||
|
*
|
||
|
* @param context context
|
||
|
* @param token token of the session to connect to
|
||
|
*/
|
||
|
public Builder(@NonNull Context context, @NonNull Session2Token token) {
|
||
|
if (context == null) {
|
||
|
throw new IllegalArgumentException("context shouldn't be null");
|
||
|
}
|
||
|
if (token == null) {
|
||
|
throw new IllegalArgumentException("token shouldn't be null");
|
||
|
}
|
||
|
mContext = context;
|
||
|
mToken = token;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set the connection hints for the controller.
|
||
|
* <p>
|
||
|
* {@code connectionHints} is a session-specific argument to send to the session when
|
||
|
* connecting. The contents of this bundle may affect the connection result.
|
||
|
* <p>
|
||
|
* An {@link IllegalArgumentException} will be thrown if the bundle contains any
|
||
|
* non-framework Parcelable objects.
|
||
|
*
|
||
|
* @param connectionHints a bundle which contains the connection hints
|
||
|
* @return The Builder to allow chaining
|
||
|
*/
|
||
|
@NonNull
|
||
|
public Builder setConnectionHints(@NonNull Bundle connectionHints) {
|
||
|
if (connectionHints == null) {
|
||
|
throw new IllegalArgumentException("connectionHints shouldn't be null");
|
||
|
}
|
||
|
if (MediaSession2.hasCustomParcelable(connectionHints)) {
|
||
|
throw new IllegalArgumentException("connectionHints shouldn't contain any custom "
|
||
|
+ "parcelables");
|
||
|
}
|
||
|
mConnectionHints = new Bundle(connectionHints);
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set callback for the controller and its executor.
|
||
|
*
|
||
|
* @param executor callback executor
|
||
|
* @param callback session callback.
|
||
|
* @return The Builder to allow chaining
|
||
|
*/
|
||
|
@NonNull
|
||
|
public Builder setControllerCallback(@NonNull Executor executor,
|
||
|
@NonNull ControllerCallback callback) {
|
||
|
if (executor == null) {
|
||
|
throw new IllegalArgumentException("executor shouldn't be null");
|
||
|
}
|
||
|
if (callback == null) {
|
||
|
throw new IllegalArgumentException("callback shouldn't be null");
|
||
|
}
|
||
|
mCallbackExecutor = executor;
|
||
|
mCallback = callback;
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Build {@link MediaController2}.
|
||
|
*
|
||
|
* @return a new controller
|
||
|
*/
|
||
|
@NonNull
|
||
|
public MediaController2 build() {
|
||
|
if (mCallbackExecutor == null) {
|
||
|
mCallbackExecutor = mContext.getMainExecutor();
|
||
|
}
|
||
|
if (mCallback == null) {
|
||
|
mCallback = new ControllerCallback() {};
|
||
|
}
|
||
|
if (mConnectionHints == null) {
|
||
|
mConnectionHints = Bundle.EMPTY;
|
||
|
}
|
||
|
return new MediaController2(
|
||
|
mContext, mToken, mConnectionHints, mCallbackExecutor, mCallback);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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>
|
||
|
* Interface for listening to change in activeness of the {@link MediaSession2}.
|
||
|
*/
|
||
|
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 the session's playback activeness is changed.
|
||
|
*
|
||
|
* @param controller the controller for this event
|
||
|
* @param playbackActive {@code true} if the session's playback is active.
|
||
|
* {@code false} otherwise.
|
||
|
* @see MediaController2#isPlaybackActive()
|
||
|
*/
|
||
|
public void onPlaybackActiveChanged(@NonNull MediaController2 controller,
|
||
|
boolean playbackActive) {}
|
||
|
|
||
|
/**
|
||
|
* Called when the connected session sent a session command.
|
||
|
*
|
||
|
* @param controller the controller for this event
|
||
|
* @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 MediaController2 controller,
|
||
|
@NonNull Session2Command command, @Nullable Bundle args) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Called when the command sent to the connected session is finished.
|
||
|
*
|
||
|
* @param controller the controller for this event
|
||
|
* @param token the token got from {@link MediaController2#sendSessionCommand}
|
||
|
* @param command the session command
|
||
|
* @param result the result of the session command
|
||
|
*/
|
||
|
public void onCommandResult(@NonNull MediaController2 controller, @NonNull Object token,
|
||
|
@NonNull Session2Command command, @NonNull Session2Command.Result result) {}
|
||
|
}
|
||
|
|
||
|
// This will be called on the main thread.
|
||
|
private class SessionServiceConnection implements ServiceConnection {
|
||
|
private final Bundle mConnectionHints;
|
||
|
|
||
|
SessionServiceConnection(@Nullable Bundle connectionHints) {
|
||
|
mConnectionHints = connectionHints;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onServiceConnected(ComponentName name, IBinder service) {
|
||
|
// Note that it's always main-thread.
|
||
|
boolean connectRequested = false;
|
||
|
try {
|
||
|
if (DEBUG) {
|
||
|
Log.d(TAG, "onServiceConnected " + name + " " + this);
|
||
|
}
|
||
|
if (!mSessionToken.getPackageName().equals(name.getPackageName())) {
|
||
|
Log.wtf(TAG, "Expected connection to " + mSessionToken.getPackageName()
|
||
|
+ " but is connected to " + name);
|
||
|
return;
|
||
|
}
|
||
|
IMediaSession2Service iService = IMediaSession2Service.Stub.asInterface(service);
|
||
|
if (iService == null) {
|
||
|
Log.wtf(TAG, "Service interface is missing.");
|
||
|
return;
|
||
|
}
|
||
|
Bundle connectionRequest = createConnectionRequest(mConnectionHints);
|
||
|
iService.connect(mControllerStub, getNextSeqNumber(), connectionRequest);
|
||
|
connectRequested = true;
|
||
|
} catch (RemoteException e) {
|
||
|
Log.w(TAG, "Service " + name + " has died prematurely", e);
|
||
|
} finally {
|
||
|
if (!connectRequested) {
|
||
|
close();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onServiceDisconnected(ComponentName name) {
|
||
|
// Temporal lose of the binding because of the service crash. System will automatically
|
||
|
// rebind, so just no-op.
|
||
|
if (DEBUG) {
|
||
|
Log.w(TAG, "Session service " + name + " is disconnected.");
|
||
|
}
|
||
|
close();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onBindingDied(ComponentName name) {
|
||
|
// Permanent lose of the binding because of the service package update or removed.
|
||
|
// This SessionServiceRecord will be removed accordingly, but forget session binder here
|
||
|
// for sure.
|
||
|
close();
|
||
|
}
|
||
|
}
|
||
|
}
|