MediaSession2Service: Initial commit
Bug: 122563346 Test: Build Change-Id: I250ee493837bfa7964fa7baf3d11f1673c879010
This commit is contained in:
@@ -476,6 +476,7 @@ java_defaults {
|
||||
"media/java/android/media/IMediaRouterClient.aidl",
|
||||
"media/java/android/media/IMediaRouterService.aidl",
|
||||
"media/java/android/media/IMediaSession2.aidl",
|
||||
"media/java/android/media/IMediaSession2Service.aidl",
|
||||
"media/java/android/media/IMediaScannerListener.aidl",
|
||||
"media/java/android/media/IMediaScannerService.aidl",
|
||||
"media/java/android/media/IPlaybackConfigDispatcher.aidl",
|
||||
|
||||
32
media/java/android/media/IMediaSession2Service.aidl
Normal file
32
media/java/android/media/IMediaSession2Service.aidl
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.media;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.media.Controller2Link;
|
||||
|
||||
/**
|
||||
* Interface from MediaController2 to MediaSession2Service.
|
||||
* <p>
|
||||
* Keep this interface oneway. Otherwise a malicious app may implement fake version of this,
|
||||
* and holds calls from controller to make controller owner(s) frozen.
|
||||
* @hide
|
||||
*/
|
||||
oneway interface IMediaSession2Service {
|
||||
void connect(in Controller2Link caller, int seq, in Bundle connectionRequest) = 0;
|
||||
// Next Id : 1
|
||||
}
|
||||
@@ -26,12 +26,16 @@ 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.Binder;
|
||||
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;
|
||||
@@ -63,6 +67,7 @@ public class MediaController2 implements AutoCloseable {
|
||||
private final Executor mCallbackExecutor;
|
||||
private final Controller2Link mControllerStub;
|
||||
private final Handler mResultHandler;
|
||||
private final SessionServiceConnection mServiceConnection;
|
||||
|
||||
private final Object mLock = new Object();
|
||||
//@GuardedBy("mLock")
|
||||
@@ -118,16 +123,25 @@ public class MediaController2 implements AutoCloseable {
|
||||
mPendingCommands = new ArrayMap<>();
|
||||
mRequestedCommandSeqNumbers = new ArraySet<>();
|
||||
|
||||
boolean connectRequested;
|
||||
if (token.getType() == TYPE_SESSION) {
|
||||
connectToSession();
|
||||
mServiceConnection = null;
|
||||
connectRequested = requestConnectToSession();
|
||||
} else {
|
||||
// TODO: Handle connect to session service.
|
||||
mServiceConnection = new SessionServiceConnection();
|
||||
connectRequested = requestConnectToService();
|
||||
}
|
||||
if (!connectRequested) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
synchronized (mLock) {
|
||||
if (mServiceConnection != null) {
|
||||
mContext.unbindService(mServiceConnection);
|
||||
}
|
||||
if (mSessionBinder != null) {
|
||||
try {
|
||||
mSessionBinder.disconnect(mControllerStub, getNextSeqNumber());
|
||||
@@ -299,18 +313,55 @@ public class MediaController2 implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
private void connectToSession() {
|
||||
Session2Link sessionBinder = mSessionToken.getSessionLink();
|
||||
private Bundle createConnectionRequest() {
|
||||
Bundle connectionRequest = new Bundle();
|
||||
connectionRequest.putString(KEY_PACKAGE_NAME, mContext.getPackageName());
|
||||
connectionRequest.putInt(KEY_PID, Process.myPid());
|
||||
return connectionRequest;
|
||||
}
|
||||
|
||||
private boolean requestConnectToSession() {
|
||||
Session2Link sessionBinder = mSessionToken.getSessionLink();
|
||||
Bundle connectionRequest = createConnectionRequest();
|
||||
try {
|
||||
sessionBinder.connect(mControllerStub, getNextSeqNumber(), connectionRequest);
|
||||
} catch (RuntimeException e) {
|
||||
Log.w(TAG, "Failed to call connection request. Framework will retry"
|
||||
+ " automatically");
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -367,4 +418,59 @@ public class MediaController2 implements AutoCloseable {
|
||||
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 {
|
||||
SessionServiceConnection() {
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
// Sanity check
|
||||
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();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.media.session.MediaSessionManager;
|
||||
import android.media.session.MediaSessionManager.RemoteUserInfo;
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Process;
|
||||
@@ -41,7 +40,6 @@ import android.util.ArraySet;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -117,15 +115,19 @@ public class MediaSession2 implements AutoCloseable {
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
List<ControllerInfo> controllerInfos;
|
||||
synchronized (mLock) {
|
||||
if (mClosed) {
|
||||
return;
|
||||
}
|
||||
mClosed = true;
|
||||
controllerInfos = getConnectedControllers();
|
||||
mConnectedControllers.clear();
|
||||
mCallback.onSessionClosed(this);
|
||||
}
|
||||
synchronized (MediaSession2.class) {
|
||||
SESSION_ID_LIST.remove(mSessionId);
|
||||
}
|
||||
Collection<ControllerInfo> controllerInfos;
|
||||
synchronized (mLock) {
|
||||
controllerInfos = mConnectedControllers.values();
|
||||
mConnectedControllers.clear();
|
||||
mClosed = true;
|
||||
}
|
||||
for (ControllerInfo info : controllerInfos) {
|
||||
info.notifyDisconnected();
|
||||
}
|
||||
@@ -160,10 +162,7 @@ public class MediaSession2 implements AutoCloseable {
|
||||
if (command == null) {
|
||||
throw new IllegalArgumentException("command shouldn't be null");
|
||||
}
|
||||
Collection<ControllerInfo> controllerInfos;
|
||||
synchronized (mLock) {
|
||||
controllerInfos = mConnectedControllers.values();
|
||||
}
|
||||
List<ControllerInfo> controllerInfos = getConnectedControllers();
|
||||
for (ControllerInfo controller : controllerInfos) {
|
||||
controller.sendSessionCommand(command, args, null);
|
||||
}
|
||||
@@ -222,23 +221,26 @@ public class MediaSession2 implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
// Called by Session2Link.onConnect
|
||||
void onConnect(final Controller2Link controller, int seq, Bundle connectionRequest) {
|
||||
if (controller == null || connectionRequest == null) {
|
||||
return;
|
||||
SessionCallback getCallback() {
|
||||
return mCallback;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
final int uid = Binder.getCallingUid();
|
||||
final int callingPid = Binder.getCallingPid();
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
// Binder.getCallingPid() can be 0 for an oneway call from the remote process.
|
||||
// If it's the case, use PID from the ConnectionRequest.
|
||||
final int pid = (callingPid != 0) ? callingPid : connectionRequest.getInt(KEY_PID);
|
||||
final String pkg = connectionRequest.getString(KEY_PACKAGE_NAME);
|
||||
try {
|
||||
RemoteUserInfo remoteUserInfo = new RemoteUserInfo(pkg, pid, uid);
|
||||
final ControllerInfo controllerInfo = new ControllerInfo(remoteUserInfo,
|
||||
mSessionManager.isTrustedForMediaControl(remoteUserInfo), controller);
|
||||
mCallbackExecutor.execute(() -> {
|
||||
String callingPkg = connectionRequest.getString(KEY_PACKAGE_NAME);
|
||||
|
||||
RemoteUserInfo remoteUserInfo = new RemoteUserInfo(callingPkg, callingPid, callingUid);
|
||||
final ControllerInfo controllerInfo = new ControllerInfo(remoteUserInfo,
|
||||
mSessionManager.isTrustedForMediaControl(remoteUserInfo), controller);
|
||||
mCallbackExecutor.execute(() -> {
|
||||
boolean accept = false;
|
||||
try {
|
||||
if (isClosed()) {
|
||||
return;
|
||||
}
|
||||
@@ -247,77 +249,67 @@ public class MediaSession2 implements AutoCloseable {
|
||||
// Don't reject connection for the request from trusted app.
|
||||
// Otherwise server will fail to retrieve session's information to dispatch
|
||||
// media keys to.
|
||||
boolean accept =
|
||||
controllerInfo.mAllowedCommands != null || controllerInfo.isTrusted();
|
||||
if (accept) {
|
||||
if (controllerInfo.mAllowedCommands == null) {
|
||||
// For trusted apps, send non-null allowed commands to keep
|
||||
// connection.
|
||||
controllerInfo.mAllowedCommands =
|
||||
new Session2CommandGroup.Builder().build();
|
||||
accept = controllerInfo.mAllowedCommands != null || controllerInfo.isTrusted();
|
||||
if (!accept) {
|
||||
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);
|
||||
}
|
||||
synchronized (mLock) {
|
||||
if (mConnectedControllers.containsKey(controller)) {
|
||||
Log.w(TAG, "Controller " + controllerInfo + " has sent connection"
|
||||
+ " request multiple times");
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Accepting connection: " + controllerInfo);
|
||||
}
|
||||
synchronized (mLock) {
|
||||
if (mConnectedControllers.containsKey(controller)) {
|
||||
Log.w(TAG, "Controller " + controllerInfo + " has sent connection"
|
||||
+ " request multiple times");
|
||||
}
|
||||
mConnectedControllers.put(controller, controllerInfo);
|
||||
}
|
||||
// If connection is accepted, notify the current state to the controller.
|
||||
// It's needed because we cannot call synchronous calls between
|
||||
// session/controller.
|
||||
Bundle connectionResult = new Bundle();
|
||||
connectionResult.putParcelable(KEY_SESSION2LINK, mSessionStub);
|
||||
connectionResult.putParcelable(KEY_ALLOWED_COMMANDS,
|
||||
controllerInfo.mAllowedCommands);
|
||||
mConnectedControllers.put(controller, controllerInfo);
|
||||
}
|
||||
// If connection is accepted, notify the current state to the controller.
|
||||
// It's needed because we cannot call synchronous calls between
|
||||
// session/controller.
|
||||
Bundle connectionResult = new Bundle();
|
||||
connectionResult.putParcelable(KEY_SESSION2LINK, mSessionStub);
|
||||
connectionResult.putParcelable(KEY_ALLOWED_COMMANDS,
|
||||
controllerInfo.mAllowedCommands);
|
||||
|
||||
// Double check if session is still there, because close() can be called in
|
||||
// another thread.
|
||||
if (isClosed()) {
|
||||
return;
|
||||
}
|
||||
controllerInfo.notifyConnected(connectionResult);
|
||||
} else {
|
||||
// Double check if session is still there, because close() can be called in
|
||||
// another thread.
|
||||
if (isClosed()) {
|
||||
return;
|
||||
}
|
||||
controllerInfo.notifyConnected(connectionResult);
|
||||
} finally {
|
||||
if (!accept) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Rejecting connection, controllerInfo=" + controllerInfo);
|
||||
}
|
||||
controllerInfo.notifyDisconnected();
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
controllerInfo.notifyDisconnected();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Called by Session2Link.onDisconnect
|
||||
void onDisconnect(final Controller2Link controller, int seq) {
|
||||
if (controller == null) {
|
||||
return;
|
||||
}
|
||||
void onDisconnect(@NonNull final Controller2Link controller, int seq) {
|
||||
final ControllerInfo controllerInfo;
|
||||
synchronized (mLock) {
|
||||
controllerInfo = mConnectedControllers.get(controller);
|
||||
controllerInfo = mConnectedControllers.remove(controller);
|
||||
}
|
||||
if (controllerInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
mCallbackExecutor.execute(() -> {
|
||||
mCallback.onDisconnected(MediaSession2.this, controllerInfo);
|
||||
});
|
||||
mConnectedControllers.remove(controller);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
mCallbackExecutor.execute(() -> {
|
||||
mCallback.onDisconnected(MediaSession2.this, controllerInfo);
|
||||
});
|
||||
}
|
||||
|
||||
// Called by Session2Link.onSessionCommand
|
||||
void onSessionCommand(final Controller2Link controller, final int seq,
|
||||
void onSessionCommand(@NonNull final Controller2Link controller, final int seq,
|
||||
final Session2Command command, final Bundle args,
|
||||
@Nullable ResultReceiver resultReceiver) {
|
||||
if (controller == null) {
|
||||
@@ -332,34 +324,28 @@ public class MediaSession2 implements AutoCloseable {
|
||||
}
|
||||
|
||||
// TODO: check allowed commands.
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
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) {
|
||||
throw new RuntimeException("onSessionCommand shouldn't return null");
|
||||
} else {
|
||||
resultReceiver.send(result.getResultCode(), result.getResultData());
|
||||
}
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
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) {
|
||||
throw new RuntimeException("onSessionCommand shouldn't return null");
|
||||
} else {
|
||||
resultReceiver.send(result.getResultCode(), result.getResultData());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Called by Session2Link.onCancelCommand
|
||||
void onCancelCommand(final Controller2Link controller, final int seq) {
|
||||
void onCancelCommand(@NonNull final Controller2Link controller, final int seq) {
|
||||
final ControllerInfo controllerInfo;
|
||||
synchronized (mLock) {
|
||||
controllerInfo = mConnectedControllers.get(controller);
|
||||
@@ -367,13 +353,15 @@ public class MediaSession2 implements AutoCloseable {
|
||||
if (controllerInfo == null) {
|
||||
return;
|
||||
}
|
||||
controllerInfo.removeRequestedCommandSeqNumber(seq);
|
||||
}
|
||||
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
controllerInfo.removeRequestedCommandSeqNumber(seq);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
private List<ControllerInfo> getConnectedControllers() {
|
||||
List<ControllerInfo> controllers = new ArrayList<>();
|
||||
synchronized (mLock) {
|
||||
controllers.addAll(mConnectedControllers.values());
|
||||
}
|
||||
return controllers;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -660,6 +648,8 @@ public class MediaSession2 implements AutoCloseable {
|
||||
* This API is not generally intended for third party application developers.
|
||||
*/
|
||||
public abstract static class SessionCallback {
|
||||
ForegroundServiceEventCallback mForegroundServiceEventCallback;
|
||||
|
||||
/**
|
||||
* Called when a controller is created for this session. Return allowed commands for
|
||||
* controller. By default it returns {@code null}.
|
||||
@@ -716,5 +706,19 @@ public class MediaSession2 implements AutoCloseable {
|
||||
public void onCommandResult(@NonNull MediaSession2 session,
|
||||
@NonNull ControllerInfo controller, @NonNull Object token,
|
||||
@NonNull Session2Command command, @NonNull Session2Command.Result result) {}
|
||||
|
||||
final void onSessionClosed(MediaSession2 session) {
|
||||
if (mForegroundServiceEventCallback != null) {
|
||||
mForegroundServiceEventCallback.onSessionClosed(session);
|
||||
}
|
||||
}
|
||||
|
||||
void setForegroundServiceEventCallback(ForegroundServiceEventCallback callback) {
|
||||
mForegroundServiceEventCallback = callback;
|
||||
}
|
||||
|
||||
abstract static class ForegroundServiceEventCallback {
|
||||
public void onSessionClosed(MediaSession2 session) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
288
media/java/android/media/MediaSession2Service.java
Normal file
288
media/java/android/media/MediaSession2Service.java
Normal file
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.media;
|
||||
|
||||
import android.annotation.CallSuper;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Service containing {@link MediaSession2}.
|
||||
* <p>
|
||||
* 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/package-summary.html">Media2 Library</a>
|
||||
* for consistent behavior across all devices.
|
||||
* @hide
|
||||
*/
|
||||
// TODO: Unhide
|
||||
// TODO: Add onUpdateNotification(), and calls it to get Notification for startForegroundService()
|
||||
// when a session's player state becomes playing.
|
||||
public abstract class MediaSession2Service extends Service {
|
||||
/**
|
||||
* The {@link Intent} that must be declared as handled by the service.
|
||||
*/
|
||||
public static final String SERVICE_INTERFACE = "android.media.MediaSession2Service";
|
||||
|
||||
private static final String TAG = "MediaSession2Service";
|
||||
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
|
||||
|
||||
private final Object mLock = new Object();
|
||||
@GuardedBy("mLock")
|
||||
private Map<String, MediaSession2> mSessions = new ArrayMap<>();
|
||||
|
||||
private MediaSession2ServiceStub mStub;
|
||||
|
||||
/**
|
||||
* Called by the system when the service is first created. Do not call this method directly.
|
||||
* <p>
|
||||
* Override this method if you need your own initialization. Derived classes MUST call through
|
||||
* to the super class's implementation of this method.
|
||||
*/
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
mStub = new MediaSession2ServiceStub(this);
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
@Nullable
|
||||
public IBinder onBind(@NonNull Intent intent) {
|
||||
if (SERVICE_INTERFACE.equals(intent.getAction())) {
|
||||
return mStub;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
// TODO: Dispatch media key events to the primary session.
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the system to notify that it is no longer used and is being removed. Do not call
|
||||
* this method directly.
|
||||
* <p>
|
||||
* Override this method if you need your own clean up. Derived classes MUST call through
|
||||
* to the super class's implementation of this method.
|
||||
*/
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
synchronized (mLock) {
|
||||
for (MediaSession2 session : mSessions.values()) {
|
||||
session.getCallback().setForegroundServiceEventCallback(null);
|
||||
}
|
||||
mSessions.clear();
|
||||
}
|
||||
mStub.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a {@link MediaController2} is created with the this service's
|
||||
* {@link Session2Token}. Return the primary session for telling the controller which session to
|
||||
* connect.
|
||||
* <p>
|
||||
* Primary session is the highest priority session that this service manages. Here are some
|
||||
* recommendations of the primary session.
|
||||
* <ol>
|
||||
* <li>When there's no {@link MediaSession2}, create and return a new session. Resume the
|
||||
* playback that the app has the lastly played with the new session. The behavior is what
|
||||
* framework expects when the framework sends key events to the service.</li>
|
||||
* <li>When there's multiple {@link MediaSession2}s, pick the session that has the lastly
|
||||
* started the playback. This is the same way as the framework prioritize sessions to receive
|
||||
* media key events.</li>
|
||||
* </ol>
|
||||
* <p>
|
||||
* Session returned here will be added to this service automatically. You don't need to call
|
||||
* {@link #addSession(MediaSession2)} for that.
|
||||
* <p>
|
||||
* Session service will accept or reject the connection with the
|
||||
* {@link MediaSession2.SessionCallback} in the session returned here.
|
||||
* <p>
|
||||
* This method is always called on the main thread.
|
||||
*
|
||||
* @return a new session
|
||||
* @see MediaSession2.Builder
|
||||
* @see #getSessions()
|
||||
*/
|
||||
@NonNull
|
||||
public abstract MediaSession2 onGetPrimarySession();
|
||||
|
||||
/**
|
||||
* Adds a session to this service.
|
||||
* <p>
|
||||
* Added session will be removed automatically when it's closed, or removed when
|
||||
* {@link #removeSession} is called.
|
||||
*
|
||||
* @param session a session to be added.
|
||||
* @see #removeSession(MediaSession2)
|
||||
*/
|
||||
public final void addSession(@NonNull MediaSession2 session) {
|
||||
if (session == null) {
|
||||
throw new IllegalArgumentException("session shouldn't be null");
|
||||
}
|
||||
if (session.isClosed()) {
|
||||
throw new IllegalArgumentException("session is already closed");
|
||||
}
|
||||
synchronized (mLock) {
|
||||
MediaSession2 previousSession = mSessions.get(session.getSessionId());
|
||||
if (previousSession != session) {
|
||||
if (previousSession != null) {
|
||||
Log.w(TAG, "Session ID should be unique, ID=" + session.getSessionId()
|
||||
+ ", previous=" + previousSession + ", session=" + session);
|
||||
}
|
||||
return;
|
||||
}
|
||||
mSessions.put(session.getSessionId(), session);
|
||||
session.getCallback().setForegroundServiceEventCallback(
|
||||
new MediaSession2.SessionCallback.ForegroundServiceEventCallback() {
|
||||
@Override
|
||||
public void onSessionClosed(MediaSession2 session) {
|
||||
removeSession(session);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a session from this service.
|
||||
*
|
||||
* @param session a session to be removed.
|
||||
* @see #addSession(MediaSession2)
|
||||
*/
|
||||
public final void removeSession(@NonNull MediaSession2 session) {
|
||||
if (session == null) {
|
||||
throw new IllegalArgumentException("session shouldn't be null");
|
||||
}
|
||||
synchronized (mLock) {
|
||||
mSessions.remove(session.getSessionId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of {@link MediaSession2}s that you've added to this service.
|
||||
*
|
||||
* @return sessions
|
||||
*/
|
||||
public final @NonNull List<MediaSession2> getSessions() {
|
||||
List<MediaSession2> list = new ArrayList<>();
|
||||
synchronized (mLock) {
|
||||
list.addAll(mSessions.values());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private static final class MediaSession2ServiceStub extends IMediaSession2Service.Stub
|
||||
implements AutoCloseable {
|
||||
final WeakReference<MediaSession2Service> mService;
|
||||
final Handler mHandler;
|
||||
|
||||
MediaSession2ServiceStub(MediaSession2Service service) {
|
||||
mService = new WeakReference<>(service);
|
||||
mHandler = new Handler(service.getMainLooper());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect(Controller2Link caller, int seq, Bundle connectionRequest) {
|
||||
if (mService.get() == null) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Service is already destroyed");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (caller == null || connectionRequest == null) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Ignoring calls with illegal arguments, caller=" + caller
|
||||
+ ", connectionRequest=" + connectionRequest);
|
||||
}
|
||||
return;
|
||||
}
|
||||
final int pid = Binder.getCallingPid();
|
||||
final int uid = Binder.getCallingUid();
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
mHandler.post(() -> {
|
||||
boolean shouldNotifyDisconnected = true;
|
||||
try {
|
||||
final MediaSession2Service service = mService.get();
|
||||
if (service == null) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Service isn't available");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Handling incoming connection request from the"
|
||||
+ " controller, controller=" + caller + ", uid=" + uid);
|
||||
}
|
||||
final MediaSession2 session;
|
||||
session = service.onGetPrimarySession();
|
||||
service.addSession(session);
|
||||
shouldNotifyDisconnected = false;
|
||||
session.onConnect(caller, pid, uid, seq, connectionRequest);
|
||||
} catch (Exception e) {
|
||||
// Don't propagate exception in service to the controller.
|
||||
Log.w(TAG, "Failed to add a session to session service", e);
|
||||
} finally {
|
||||
// Trick to call onDisconnected() in one place.
|
||||
if (shouldNotifyDisconnected) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Service has destroyed prematurely."
|
||||
+ " Rejecting connection");
|
||||
}
|
||||
try {
|
||||
caller.notifyDisconnected(0);
|
||||
} catch (RuntimeException e) {
|
||||
// Controller may be died prematurely.
|
||||
// Not an issue because we'll ignore it anyway.
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
mHandler.removeCallbacksAndMessages(null);
|
||||
mService.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
package android.media;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.Parcel;
|
||||
@@ -145,8 +146,9 @@ public final class Session2Link implements Parcelable {
|
||||
}
|
||||
|
||||
/** Stub implementation for IMediaSession2.connect */
|
||||
public void onConnect(final Controller2Link caller, int seq, Bundle connectionRequest) {
|
||||
mSession.onConnect(caller, seq, connectionRequest);
|
||||
public void onConnect(final Controller2Link caller, int pid, int uid, int seq,
|
||||
Bundle connectionRequest) {
|
||||
mSession.onConnect(caller, pid, uid, seq, connectionRequest);
|
||||
}
|
||||
|
||||
/** Stub implementation for IMediaSession2.disconnect */
|
||||
@@ -168,23 +170,57 @@ public final class Session2Link implements Parcelable {
|
||||
private class Session2Stub extends IMediaSession2.Stub {
|
||||
@Override
|
||||
public void connect(final Controller2Link caller, int seq, Bundle connectionRequest) {
|
||||
Session2Link.this.onConnect(caller, seq, connectionRequest);
|
||||
if (caller == null || connectionRequest == null) {
|
||||
return;
|
||||
}
|
||||
final int pid = Binder.getCallingPid();
|
||||
final int uid = Binder.getCallingUid();
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
Session2Link.this.onConnect(caller, pid, uid, seq, connectionRequest);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect(final Controller2Link caller, int seq) {
|
||||
Session2Link.this.onDisconnect(caller, seq);
|
||||
if (caller == null) {
|
||||
return;
|
||||
}
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
Session2Link.this.onDisconnect(caller, seq);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendSessionCommand(final Controller2Link caller, final int seq,
|
||||
final Session2Command command, final Bundle args, ResultReceiver resultReceiver) {
|
||||
Session2Link.this.onSessionCommand(caller, seq, command, args, resultReceiver);
|
||||
if (caller == null) {
|
||||
return;
|
||||
}
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
Session2Link.this.onSessionCommand(caller, seq, command, args, resultReceiver);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelSessionCommand(final Controller2Link caller, final int seq) {
|
||||
Session2Link.this.onCancelCommand(caller, seq);
|
||||
if (caller == null) {
|
||||
return;
|
||||
}
|
||||
final long token = Binder.clearCallingIdentity();
|
||||
try {
|
||||
Session2Link.this.onCancelCommand(caller, seq);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user