summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Santiago Seifert <aquilescanta@google.com> 2024-12-12 22:42:23 +0000
committer Santiago Seifert <aquilescanta@google.com> 2024-12-17 16:21:04 +0000
commitc9617d35ea4e0323d3fb639a9d734072607e24f4 (patch)
treee508cdde5a68c999a9229287eba12a3887cb8494
parent3625d8bb139cac0f8fd603a2e534b187c3c5752f (diff)
Implement baseline system media session management
This change enables the selection of system media routes for any app, and implements the most important bits of session management, but doesn't cover everything (for example, this change doesn't cover stream expansion for system media routing sessions, or routing of the entire system). Bug: b/374321232 Test: atest CtsMediaBetterTogetherTestCases CtsMediaHostTestCasts Flag: com.android.media.flags.enable_mirroring_in_media_router_2 Change-Id: Id43c4568350db068f929d5c6463a079ee28a3471
-rw-r--r--media/java/android/media/MediaRoute2ProviderService.java25
-rw-r--r--services/core/java/com/android/server/media/MediaRoute2Provider.java12
-rw-r--r--services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java114
-rw-r--r--services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java24
-rw-r--r--services/core/java/com/android/server/media/SystemMediaRoute2Provider.java21
-rw-r--r--services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java432
6 files changed, 572 insertions, 56 deletions
diff --git a/media/java/android/media/MediaRoute2ProviderService.java b/media/java/android/media/MediaRoute2ProviderService.java
index 09f40e005b4c..60584d9c6f72 100644
--- a/media/java/android/media/MediaRoute2ProviderService.java
+++ b/media/java/android/media/MediaRoute2ProviderService.java
@@ -358,7 +358,9 @@ public abstract class MediaRoute2ProviderService extends Service {
* @return a {@link MediaStreams} instance that holds the media streams to route as part of the
* newly created routing session. May be null if system media capture failed, in which case
* you can ignore the return value, as you will receive a call to {@link #onReleaseSession}
- * where you can clean up this session
+ * where you can clean up this session. {@link AudioRecord#startRecording()} must be called
+ * immediately on {@link MediaStreams#getAudioRecord()} after calling this method, in order
+ * to start streaming audio to the receiver.
* @hide
*/
// TODO: b/362507305 - Unhide once the implementation and CTS are in place.
@@ -458,7 +460,6 @@ public abstract class MediaRoute2ProviderService extends Service {
if (uid != Process.INVALID_UID) {
audioMixingRuleBuilder.addMixRule(AudioMixingRule.RULE_MATCH_UID, uid);
}
-
AudioMix mix =
new AudioMix.Builder(audioMixingRuleBuilder.build())
.setFormat(audioFormat)
@@ -471,7 +472,11 @@ public abstract class MediaRoute2ProviderService extends Service {
Log.e(TAG, "Couldn't fetch the audio manager.");
return;
}
- audioManager.registerAudioPolicy(audioPolicy);
+ int audioPolicyResult = audioManager.registerAudioPolicy(audioPolicy);
+ if (audioPolicyResult != AudioManager.SUCCESS) {
+ Log.e(TAG, "Failed to register the audio policy.");
+ return;
+ }
var audioRecord = audioPolicy.createAudioRecordSink(mix);
if (audioRecord == null) {
Log.e(TAG, "Audio record creation failed.");
@@ -540,17 +545,19 @@ public abstract class MediaRoute2ProviderService extends Service {
}
/** Releases any system media routing resources associated with the given {@code sessionId}. */
- private void maybeReleaseMediaStreams(String sessionId) {
+ private boolean maybeReleaseMediaStreams(String sessionId) {
if (!Flags.enableMirroringInMediaRouter2()) {
- return;
+ return false;
}
synchronized (mSessionLock) {
var streams = mOngoingMediaStreams.remove(sessionId);
if (streams != null) {
releaseAudioStream(streams.mAudioPolicy, streams.mAudioRecord);
// TODO: b/380431086: Release the video stream once implemented.
+ return true;
}
}
+ return false;
}
// We cannot reach the code that requires MODIFY_AUDIO_ROUTING without holding it.
@@ -1019,12 +1026,12 @@ public abstract class MediaRoute2ProviderService extends Service {
if (!checkCallerIsSystem()) {
return;
}
- if (!checkSessionIdIsValid(sessionId, "releaseSession")) {
- return;
- }
// We proactively release the system media routing once the system requests it, to
// ensure it happens immediately.
- maybeReleaseMediaStreams(sessionId);
+ if (!maybeReleaseMediaStreams(sessionId)
+ && !checkSessionIdIsValid(sessionId, "releaseSession")) {
+ return;
+ }
addRequestId(requestId);
mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onReleaseSession,
diff --git a/services/core/java/com/android/server/media/MediaRoute2Provider.java b/services/core/java/com/android/server/media/MediaRoute2Provider.java
index 58c8450d714d..0438a1bac662 100644
--- a/services/core/java/com/android/server/media/MediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/MediaRoute2Provider.java
@@ -21,6 +21,7 @@ import android.annotation.Nullable;
import android.content.ComponentName;
import android.media.MediaRoute2Info;
import android.media.MediaRoute2ProviderInfo;
+import android.media.MediaRoute2ProviderService.Reason;
import android.media.MediaRouter2;
import android.media.MediaRouter2Utils;
import android.media.RouteDiscoveryPreference;
@@ -123,6 +124,13 @@ abstract class MediaRoute2Provider {
}
}
+ /** Calls {@link Callback#onRequestFailed} with the given id and reason. */
+ protected void notifyRequestFailed(long requestId, @Reason int reason) {
+ if (mCallback != null) {
+ mCallback.onRequestFailed(/* provider= */ this, requestId, reason);
+ }
+ }
+
void setAndNotifyProviderState(MediaRoute2ProviderInfo providerInfo) {
setProviderState(providerInfo);
notifyProviderState();
@@ -175,7 +183,9 @@ abstract class MediaRoute2Provider {
@NonNull RoutingSessionInfo sessionInfo);
void onSessionReleased(@NonNull MediaRoute2Provider provider,
@NonNull RoutingSessionInfo sessionInfo);
- void onRequestFailed(@NonNull MediaRoute2Provider provider, long requestId, int reason);
+
+ void onRequestFailed(
+ @NonNull MediaRoute2Provider provider, long requestId, @Reason int reason);
}
/**
diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
index f09be2c15ee0..80d3c5c5c5ec 100644
--- a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
+++ b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
@@ -31,6 +31,7 @@ import android.media.IMediaRoute2ProviderServiceCallback;
import android.media.MediaRoute2Info;
import android.media.MediaRoute2ProviderInfo;
import android.media.MediaRoute2ProviderService;
+import android.media.MediaRoute2ProviderService.Reason;
import android.media.RouteDiscoveryPreference;
import android.media.RoutingSessionInfo;
import android.os.Bundle;
@@ -41,6 +42,7 @@ import android.os.Looper;
import android.os.RemoteException;
import android.os.UserHandle;
import android.text.TextUtils;
+import android.util.ArrayMap;
import android.util.Log;
import android.util.LongSparseArray;
import android.util.Slog;
@@ -89,6 +91,12 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider {
mRequestIdToSessionCreationRequest;
@GuardedBy("mLock")
+ private final Map<String, SystemMediaSessionCallback> mSystemSessionCallbacks;
+
+ @GuardedBy("mLock")
+ private final LongSparseArray<SystemMediaSessionCallback> mRequestIdToSystemSessionRequest;
+
+ @GuardedBy("mLock")
private final Map<String, SessionCreationOrTransferRequest> mSessionOriginalIdToTransferRequest;
MediaRoute2ProviderServiceProxy(
@@ -102,6 +110,8 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider {
mContext = Objects.requireNonNull(context, "Context must not be null.");
mRequestIdToSessionCreationRequest = new LongSparseArray<>();
mSessionOriginalIdToTransferRequest = new HashMap<>();
+ mRequestIdToSystemSessionRequest = new LongSparseArray<>();
+ mSystemSessionCallbacks = new ArrayMap<>();
mIsSelfScanOnlyProvider = isSelfScanOnlyProvider;
mSupportsSystemMediaRouting = supportsSystemMediaRouting;
mUserId = userId;
@@ -236,6 +246,48 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider {
}
}
+ /**
+ * Requests the creation of a system media routing session.
+ *
+ * @param requestId The id of the request.
+ * @param uid The uid of the package whose media to route, or {@link
+ * android.os.Process#INVALID_UID} if not applicable (for example, if all the system's media
+ * must be routed).
+ * @param packageName The package name to populate {@link
+ * RoutingSessionInfo#getClientPackageName()}.
+ * @param routeId The id of the route to be initially {@link
+ * RoutingSessionInfo#getSelectedRoutes()}.
+ * @param sessionHints An optional bundle with paramets.
+ * @param callback A {@link SystemMediaSessionCallback} to notify of session events.
+ * @see MediaRoute2ProviderService#onCreateSystemRoutingSession
+ */
+ public void requestCreateSystemMediaSession(
+ long requestId,
+ int uid,
+ String packageName,
+ String routeId,
+ @Nullable Bundle sessionHints,
+ @NonNull SystemMediaSessionCallback callback) {
+ if (!Flags.enableMirroringInMediaRouter2()) {
+ throw new IllegalStateException(
+ "Unexpected call to requestCreateSystemMediaSession. Governing flag is"
+ + " disabled.");
+ }
+ if (mConnectionReady) {
+ boolean binderRequestSucceeded =
+ mActiveConnection.requestCreateSystemMediaSession(
+ requestId, uid, packageName, routeId, sessionHints);
+ if (!binderRequestSucceeded) {
+ // notify failure.
+ return;
+ }
+ updateBinding();
+ synchronized (mLock) {
+ mRequestIdToSystemSessionRequest.put(requestId, callback);
+ }
+ }
+ }
+
public boolean hasComponentName(String packageName, String className) {
return mComponentName.getPackageName().equals(packageName)
&& mComponentName.getClassName().equals(className);
@@ -292,7 +344,14 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider {
mLastDiscoveryPreference != null
&& mLastDiscoveryPreference.shouldPerformActiveScan()
&& mSupportsSystemMediaRouting;
+ boolean bindDueToOngoingSystemMediaRoutingSessions = false;
+ if (Flags.enableMirroringInMediaRouter2()) {
+ synchronized (mLock) {
+ bindDueToOngoingSystemMediaRoutingSessions = !mSystemSessionCallbacks.isEmpty();
+ }
+ }
if (!getSessionInfos().isEmpty()
+ || bindDueToOngoingSystemMediaRoutingSessions
|| bindDueToManagerScan
|| bindDueToSystemMediaRoutingSupport) {
return true;
@@ -438,6 +497,13 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider {
String newSessionId = newSession.getId();
synchronized (mLock) {
+ var systemMediaSessionCallback = mRequestIdToSystemSessionRequest.get(requestId);
+ if (systemMediaSessionCallback != null) {
+ mSystemSessionCallbacks.put(newSession.getOriginalId(), systemMediaSessionCallback);
+ systemMediaSessionCallback.onSessionUpdate(newSession);
+ return;
+ }
+
if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) {
newSession =
createSessionWithPopulatedTransferInitiationDataLocked(
@@ -569,6 +635,12 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider {
boolean found = false;
synchronized (mLock) {
+ var sessionCallback = mSystemSessionCallbacks.get(releasedSession.getOriginalId());
+ if (sessionCallback != null) {
+ sessionCallback.onSessionReleased();
+ return;
+ }
+
mSessionOriginalIdToTransferRequest.remove(releasedSession.getId());
for (RoutingSessionInfo session : mSessionInfos) {
if (TextUtils.equals(session.getId(), releasedSession.getId())) {
@@ -673,6 +745,26 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider {
pendingTransferCount);
}
+ /**
+ * Callback for events related to system media sessions.
+ *
+ * @see MediaRoute2ProviderService#onCreateSystemRoutingSession
+ */
+ public interface SystemMediaSessionCallback {
+
+ /**
+ * Called when the corresponding session's {@link RoutingSessionInfo}, or upon the creation
+ * of the given session info.
+ */
+ void onSessionUpdate(@NonNull RoutingSessionInfo sessionInfo);
+
+ /** Called when the request with the given id fails for the given reason. */
+ void onRequestFailed(long requestId, @Reason int reason);
+
+ /** Called when the corresponding session is released. */
+ void onSessionReleased();
+ }
+
// All methods in this class are called on the main thread.
private final class ServiceConnectionImpl implements ServiceConnection {
@@ -739,6 +831,28 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider {
}
}
+ /**
+ * Sends a system media session creation request to the provider service, and returns
+ * whether the request transaction succeeded.
+ *
+ * <p>The transaction might fail, for example, if the recipient process has died.
+ */
+ public boolean requestCreateSystemMediaSession(
+ long requestId,
+ int uid,
+ String packageName,
+ String routeId,
+ @Nullable Bundle sessionHints) {
+ try {
+ mService.requestCreateSystemMediaSession(
+ requestId, uid, packageName, routeId, sessionHints);
+ return true;
+ } catch (RemoteException ex) {
+ Slog.e(TAG, "requestCreateSystemMediaSession: Failed to deliver request.");
+ }
+ return false;
+ }
+
public void releaseSession(long requestId, String sessionId) {
try {
mService.releaseSession(requestId, sessionId);
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index 58deffcbd4ba..83ac05d9d4c3 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -846,33 +846,29 @@ class MediaRouter2ServiceImpl {
try {
synchronized (mLock) {
UserRecord userRecord = getOrCreateUserRecordLocked(userId);
- List<RoutingSessionInfo> sessionInfos;
+ SystemMediaRoute2Provider systemProvider = userRecord.mHandler.getSystemProvider();
if (hasSystemRoutingPermissions) {
- if (setDeviceRouteSelected && !Flags.enableMirroringInMediaRouter2()) {
+ if (!Flags.enableMirroringInMediaRouter2() && setDeviceRouteSelected) {
// Return a fake system session that shows the device route as selected and
// available bluetooth routes as transferable.
- return userRecord.mHandler.getSystemProvider()
- .generateDeviceRouteSelectedSessionInfo(targetPackageName);
+ return systemProvider.generateDeviceRouteSelectedSessionInfo(
+ targetPackageName);
} else {
- sessionInfos = userRecord.mHandler.getSystemProvider().getSessionInfos();
- if (!sessionInfos.isEmpty()) {
- // Return a copy of the current system session with no modification,
- // except setting the client package name.
- return new RoutingSessionInfo.Builder(sessionInfos.get(0))
- .setClientPackageName(targetPackageName)
- .build();
+ RoutingSessionInfo session =
+ systemProvider.getSessionForPackage(targetPackageName);
+ if (session != null) {
+ return session;
} else {
Slog.w(TAG, "System provider does not have any session info.");
+ return null;
}
}
} else {
- return new RoutingSessionInfo.Builder(
- userRecord.mHandler.getSystemProvider().getDefaultSessionInfo())
+ return new RoutingSessionInfo.Builder(systemProvider.getDefaultSessionInfo())
.setClientPackageName(targetPackageName)
.build();
}
}
- return null;
} finally {
Binder.restoreCallingIdentity(token);
}
diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
index b93846bf9ee7..4aec3678af8b 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
@@ -327,6 +327,23 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
}
/**
+ * Returns the {@link RoutingSessionInfo} that corresponds to the package with the given name.
+ */
+ public RoutingSessionInfo getSessionForPackage(String targetPackageName) {
+ synchronized (mLock) {
+ if (!mSessionInfos.isEmpty()) {
+ // Return a copy of the current system session with no modification,
+ // except setting the client package name.
+ return new RoutingSessionInfo.Builder(mSessionInfos.get(0))
+ .setClientPackageName(targetPackageName)
+ .build();
+ } else {
+ return null;
+ }
+ }
+ }
+
+ /**
* Builds a system {@link RoutingSessionInfo} with the selected route set to the currently
* selected <b>device</b> route (wired or built-in, but not bluetooth) and transferable routes
* set to the currently available (connected) bluetooth routes.
@@ -633,10 +650,10 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
RoutingSessionInfo sessionInfo;
synchronized (mLock) {
- sessionInfo = mSessionInfos.get(0);
- if (sessionInfo == null) {
+ if (mSessionInfos.isEmpty()) {
return;
}
+ sessionInfo = mSessionInfos.get(0);
}
mCallback.onSessionUpdated(this, sessionInfo);
diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java
index 7dc30ab66fd2..a27a14b87d53 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java
@@ -18,22 +18,31 @@ package com.android.server.media;
import static android.media.MediaRoute2Info.FEATURE_LIVE_AUDIO;
+import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.media.MediaRoute2Info;
import android.media.MediaRoute2ProviderInfo;
import android.media.MediaRoute2ProviderService;
+import android.media.MediaRoute2ProviderService.Reason;
+import android.media.MediaRouter2Utils;
import android.media.RoutingSessionInfo;
+import android.os.Binder;
import android.os.Looper;
+import android.os.Process;
import android.os.UserHandle;
-import android.util.ArraySet;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.LongSparseArray;
import com.android.internal.annotations.GuardedBy;
+import com.android.server.media.MediaRoute2ProviderServiceProxy.SystemMediaSessionCallback;
-import java.util.Collection;
import java.util.Collections;
-import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
@@ -48,11 +57,33 @@ import java.util.stream.Stream;
private static final String ROUTE_ID_PREFIX_SYSTEM = "SYSTEM";
private static final String ROUTE_ID_SYSTEM_SEPARATOR = ".";
+ private final PackageManager mPackageManager;
+
@GuardedBy("mLock")
private MediaRoute2ProviderInfo mLastSystemProviderInfo;
@GuardedBy("mLock")
- private final Map<String, ProviderProxyRecord> mProxyRecords = new HashMap<>();
+ private final Map<String, ProviderProxyRecord> mProxyRecords = new ArrayMap<>();
+
+ /**
+ * Maps package names to corresponding sessions maintained by {@link MediaRoute2ProviderService
+ * provider services}.
+ */
+ @GuardedBy("mLock")
+ private final Map<String, SystemMediaSessionRecord> mPackageNameToSessionRecord =
+ new ArrayMap<>();
+
+ /**
+ * Maps route {@link MediaRoute2Info#getOriginalId original ids} to the id of the {@link
+ * MediaRoute2ProviderService provider service} that manages the corresponding route.
+ */
+ @GuardedBy("mLock")
+ private final Map<String, String> mOriginalRouteIdToProviderId = new ArrayMap<>();
+
+ /** Maps request ids to pending session creation callbacks. */
+ @GuardedBy("mLock")
+ private final LongSparseArray<PendingSessionCreationCallbackImpl> mPendingSessionCreations =
+ new LongSparseArray<>();
private static final ComponentName COMPONENT_NAME =
new ComponentName(
@@ -69,6 +100,128 @@ import java.util.stream.Stream;
private SystemMediaRoute2Provider2(Context context, UserHandle user, Looper looper) {
super(context, COMPONENT_NAME, user, looper);
+ mPackageManager = context.getPackageManager();
+ }
+
+ @Override
+ public void transferToRoute(
+ long requestId,
+ @NonNull UserHandle clientUserHandle,
+ @NonNull String clientPackageName,
+ String sessionOriginalId,
+ String routeOriginalId,
+ int transferReason) {
+ synchronized (mLock) {
+ var targetProviderProxyId = mOriginalRouteIdToProviderId.get(routeOriginalId);
+ var targetProviderProxyRecord = mProxyRecords.get(targetProviderProxyId);
+ // Holds the target route, if it's managed by a provider service. Holds null otherwise.
+ var serviceTargetRoute =
+ targetProviderProxyRecord != null
+ ? targetProviderProxyRecord.getRouteByOriginalId(routeOriginalId)
+ : null;
+ var existingSessionRecord = mPackageNameToSessionRecord.get(clientPackageName);
+ if (existingSessionRecord != null) {
+ var existingSession = existingSessionRecord.mSourceSessionInfo;
+ if (targetProviderProxyId != null
+ && TextUtils.equals(
+ targetProviderProxyId, existingSession.getProviderId())) {
+ // The currently selected route and target route both belong to the same
+ // provider. We tell the provider to handle the transfer.
+ targetProviderProxyRecord.requestTransfer(
+ existingSession.getOriginalId(), serviceTargetRoute);
+ } else {
+ // The target route is handled by a provider other than the target one. We need
+ // to release the existing session.
+ var currentProxyRecord = existingSessionRecord.getProxyRecord();
+ if (currentProxyRecord != null) {
+ currentProxyRecord.releaseSession(
+ requestId, existingSession.getOriginalId());
+ existingSessionRecord.removeSelfFromSessionMap();
+ }
+ }
+ }
+
+ if (serviceTargetRoute != null) {
+ boolean isGlobalSession = TextUtils.isEmpty(clientPackageName);
+ int uid;
+ if (isGlobalSession) {
+ uid = Process.INVALID_UID;
+ } else {
+ uid = fetchUid(clientPackageName, clientUserHandle);
+ if (uid == Process.INVALID_UID) {
+ throw new IllegalArgumentException(
+ "Cannot resolve transfer for "
+ + clientPackageName
+ + " and "
+ + clientUserHandle);
+ }
+ }
+ var pendingCreationCallback =
+ new PendingSessionCreationCallbackImpl(
+ targetProviderProxyId, requestId, clientPackageName);
+ mPendingSessionCreations.put(requestId, pendingCreationCallback);
+ targetProviderProxyRecord.requestCreateSystemMediaSession(
+ requestId,
+ uid,
+ clientPackageName,
+ routeOriginalId,
+ pendingCreationCallback);
+ } else {
+ // The target route is not provided by any of the services. Assume it's a system
+ // provided route.
+ super.transferToRoute(
+ requestId,
+ clientUserHandle,
+ clientPackageName,
+ sessionOriginalId,
+ routeOriginalId,
+ transferReason);
+ }
+ }
+ }
+
+ @Nullable
+ @Override
+ public RoutingSessionInfo getSessionForPackage(String packageName) {
+ synchronized (mLock) {
+ var systemSession = super.getSessionForPackage(packageName);
+ if (systemSession == null) {
+ return null;
+ }
+ var overridingSession = mPackageNameToSessionRecord.get(packageName);
+ if (overridingSession != null) {
+ var builder =
+ new RoutingSessionInfo.Builder(overridingSession.mTranslatedSessionInfo)
+ .setProviderId(mUniqueId)
+ .setSystemSession(true);
+ for (var systemRoute : mLastSystemProviderInfo.getRoutes()) {
+ builder.addTransferableRoute(systemRoute.getOriginalId());
+ }
+ return builder.build();
+ } else {
+ return systemSession;
+ }
+ }
+ }
+
+ /**
+ * Returns the uid that corresponds to the given name and user handle, or {@link
+ * Process#INVALID_UID} if a uid couldn't be found.
+ */
+ @SuppressLint("MissingPermission")
+ // We clear the calling identity before calling the package manager, and we are running on the
+ // system_server.
+ private int fetchUid(String clientPackageName, UserHandle clientUserHandle) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ return mPackageManager.getApplicationInfoAsUser(
+ clientPackageName, /* flags= */ 0, clientUserHandle)
+ .uid;
+ } catch (PackageManager.NameNotFoundException e) {
+ return Process.INVALID_UID;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
}
@Override
@@ -85,7 +238,7 @@ import java.util.stream.Stream;
} else {
mProxyRecords.put(serviceProxy.mUniqueId, proxyRecord);
}
- setProviderState(buildProviderInfo());
+ updateProviderInfo();
}
updateSessionInfo();
notifyProviderState();
@@ -96,7 +249,7 @@ import java.util.stream.Stream;
public void onSystemProviderRoutesChanged(MediaRoute2ProviderInfo providerInfo) {
synchronized (mLock) {
mLastSystemProviderInfo = providerInfo;
- setProviderState(buildProviderInfo());
+ updateProviderInfo();
}
updateSessionInfo();
notifySessionInfoUpdated();
@@ -116,10 +269,13 @@ import java.util.stream.Stream;
var builder = new RoutingSessionInfo.Builder(systemSessionInfo);
mProxyRecords.values().stream()
.flatMap(ProviderProxyRecord::getRoutesStream)
- .map(MediaRoute2Info::getId)
+ .map(MediaRoute2Info::getOriginalId)
.forEach(builder::addTransferableRoute);
mSessionInfos.clear();
mSessionInfos.add(builder.build());
+ for (var sessionRecords : mPackageNameToSessionRecord.values()) {
+ mSessionInfos.add(sessionRecords.mTranslatedSessionInfo);
+ }
}
}
@@ -129,13 +285,47 @@ import java.util.stream.Stream;
* provider services}.
*/
@GuardedBy("mLock")
- private MediaRoute2ProviderInfo buildProviderInfo() {
+ private void updateProviderInfo() {
MediaRoute2ProviderInfo.Builder builder =
new MediaRoute2ProviderInfo.Builder(mLastSystemProviderInfo);
- mProxyRecords.values().stream()
- .flatMap(ProviderProxyRecord::getRoutesStream)
- .forEach(builder::addRoute);
- return builder.build();
+ mOriginalRouteIdToProviderId.clear();
+ for (var proxyRecord : mProxyRecords.values()) {
+ String proxyId = proxyRecord.mProxy.mUniqueId;
+ proxyRecord
+ .getRoutesStream()
+ .forEach(
+ route -> {
+ builder.addRoute(route);
+ mOriginalRouteIdToProviderId.put(route.getOriginalId(), proxyId);
+ });
+ }
+ setProviderState(builder.build());
+ }
+
+ /**
+ * Equivalent to {@link #asSystemRouteId}, except it takes a unique route id instead of a
+ * original id.
+ */
+ private static String uniqueIdAsSystemRouteId(String providerId, String uniqueRouteId) {
+ return asSystemRouteId(providerId, MediaRouter2Utils.getOriginalId(uniqueRouteId));
+ }
+
+ /**
+ * Returns a unique {@link MediaRoute2Info#getOriginalId() original id} for this provider to
+ * publish system media routes from {@link MediaRoute2ProviderService provider services}.
+ *
+ * <p>This provider will publish system media routes as part of the system routing session.
+ * However, said routes may also support {@link MediaRoute2Info#FLAG_ROUTING_TYPE_REMOTE remote
+ * routing}, meaning we cannot use the same id, or there would be an id collision. As a result,
+ * we derive a {@link MediaRoute2Info#getOriginalId original id} that is unique among all
+ * original route ids used by this provider.
+ */
+ private static String asSystemRouteId(String providerId, String originalRouteId) {
+ return ROUTE_ID_PREFIX_SYSTEM
+ + ROUTE_ID_SYSTEM_SEPARATOR
+ + providerId
+ + ROUTE_ID_SYSTEM_SEPARATOR
+ + originalRouteId;
}
/**
@@ -145,14 +335,69 @@ import java.util.stream.Stream;
* @param mProxy The corresponding {@link MediaRoute2ProviderServiceProxy}.
* @param mSystemMediaRoutes The last snapshot of routes from the service that support system
* media routing, as defined by {@link MediaRoute2Info#supportsSystemMediaRouting()}.
+ * @param mNewOriginalIdToSourceOriginalIdMap Maps the {@link #mSystemMediaRoutes} ids to the
+ * original ids of corresponding {@link MediaRoute2ProviderService service} route.
*/
private record ProviderProxyRecord(
MediaRoute2ProviderServiceProxy mProxy,
- Collection<MediaRoute2Info> mSystemMediaRoutes) {
+ Map<String, MediaRoute2Info> mSystemMediaRoutes,
+ Map<String, String> mNewOriginalIdToSourceOriginalIdMap) {
/** Returns a stream representation of the {@link #mSystemMediaRoutes}. */
public Stream<MediaRoute2Info> getRoutesStream() {
- return mSystemMediaRoutes.stream();
+ return mSystemMediaRoutes.values().stream();
+ }
+
+ @Nullable
+ public MediaRoute2Info getRouteByOriginalId(String routeOriginalId) {
+ return mSystemMediaRoutes.get(routeOriginalId);
+ }
+
+ /**
+ * Requests the creation of a system media routing session.
+ *
+ * @param requestId The request id.
+ * @param uid The uid of the package whose media to route, or {@link Process#INVALID_UID} if
+ * not applicable.
+ * @param packageName The name of the package whose media to route.
+ * @param originalRouteId The {@link MediaRoute2Info#getOriginalId() original route id} of
+ * the route that should be initially selected.
+ * @param callback A {@link MediaRoute2ProviderServiceProxy.SystemMediaSessionCallback} for
+ * events.
+ * @see MediaRoute2ProviderService#onCreateSystemRoutingSession
+ */
+ public void requestCreateSystemMediaSession(
+ long requestId,
+ int uid,
+ String packageName,
+ String originalRouteId,
+ SystemMediaSessionCallback callback) {
+ var targetRouteId = mNewOriginalIdToSourceOriginalIdMap.get(originalRouteId);
+ if (targetRouteId == null) {
+ Log.w(
+ TAG,
+ "Failed system media session creation due to lack of mapping for id: "
+ + originalRouteId);
+ callback.onRequestFailed(
+ requestId, MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE);
+ } else {
+ mProxy.requestCreateSystemMediaSession(
+ requestId,
+ uid,
+ packageName,
+ targetRouteId,
+ /* sessionHints= */ null,
+ callback);
+ }
+ }
+
+ public void requestTransfer(String sessionId, MediaRoute2Info targetRoute) {
+ // TODO: Map the target route to the source route original id.
+ throw new UnsupportedOperationException("TODO Implement");
+ }
+
+ public void releaseSession(long requestId, String originalSessionId) {
+ mProxy.releaseSession(requestId, originalSessionId);
}
/**
@@ -165,22 +410,149 @@ import java.util.stream.Stream;
if (providerInfo == null) {
return null;
}
- ArraySet<MediaRoute2Info> routes = new ArraySet<>();
- providerInfo.getRoutes().stream()
- .filter(MediaRoute2Info::supportsSystemMediaRouting)
- .forEach(
- route -> {
- String id =
- ROUTE_ID_PREFIX_SYSTEM
- + route.getProviderId()
- + ROUTE_ID_SYSTEM_SEPARATOR
- + route.getOriginalId();
- routes.add(
- new MediaRoute2Info.Builder(id, route.getName())
- .addFeature(FEATURE_LIVE_AUDIO)
- .build());
- });
- return new ProviderProxyRecord(serviceProxy, Collections.unmodifiableSet(routes));
+ Map<String, MediaRoute2Info> routesMap = new ArrayMap<>();
+ Map<String, String> idMap = new ArrayMap<>();
+ for (MediaRoute2Info sourceRoute : providerInfo.getRoutes()) {
+ if (!sourceRoute.supportsSystemMediaRouting()) {
+ continue;
+ }
+ String id =
+ asSystemRouteId(providerInfo.getUniqueId(), sourceRoute.getOriginalId());
+ var newRoute =
+ new MediaRoute2Info.Builder(id, sourceRoute.getName())
+ .addFeature(FEATURE_LIVE_AUDIO)
+ .build();
+ routesMap.put(id, newRoute);
+ idMap.put(id, sourceRoute.getOriginalId());
+ }
+ return new ProviderProxyRecord(
+ serviceProxy,
+ Collections.unmodifiableMap(routesMap),
+ Collections.unmodifiableMap(idMap));
+ }
+ }
+
+ private class PendingSessionCreationCallbackImpl implements SystemMediaSessionCallback {
+
+ private final String mProviderId;
+ private final long mRequestId;
+ private final String mClientPackageName;
+
+ private PendingSessionCreationCallbackImpl(
+ String providerId, long requestId, String clientPackageName) {
+ mProviderId = providerId;
+ mRequestId = requestId;
+ mClientPackageName = clientPackageName;
+ }
+
+ @Override
+ public void onSessionUpdate(RoutingSessionInfo sessionInfo) {
+ SystemMediaSessionRecord systemMediaSessionRecord =
+ new SystemMediaSessionRecord(mProviderId, sessionInfo);
+ synchronized (mLock) {
+ mPackageNameToSessionRecord.put(mClientPackageName, systemMediaSessionRecord);
+ mPendingSessionCreations.remove(mRequestId);
+ }
+ }
+
+ @Override
+ public void onRequestFailed(long requestId, @Reason int reason) {
+ synchronized (mLock) {
+ mPendingSessionCreations.remove(mRequestId);
+ }
+ notifyRequestFailed(requestId, reason);
+ }
+
+ @Override
+ public void onSessionReleased() {
+ // Unexpected. The session hasn't yet been created.
+ throw new IllegalStateException();
+ }
+ }
+
+ private class SystemMediaSessionRecord implements SystemMediaSessionCallback {
+
+ private final String mProviderId;
+
+ @GuardedBy("SystemMediaRoute2Provider2.this.mLock")
+ @NonNull
+ private RoutingSessionInfo mSourceSessionInfo;
+
+ /**
+ * The same as {@link #mSourceSessionInfo}, except ids are {@link #asSystemRouteId system
+ * provider ids}.
+ */
+ @GuardedBy("SystemMediaRoute2Provider2.this.mLock")
+ @NonNull
+ private RoutingSessionInfo mTranslatedSessionInfo;
+
+ SystemMediaSessionRecord(
+ @NonNull String providerId, @NonNull RoutingSessionInfo sessionInfo) {
+ mProviderId = providerId;
+ mSourceSessionInfo = sessionInfo;
+ mTranslatedSessionInfo = asSystemProviderSession(sessionInfo);
+ }
+
+ @Override
+ public void onSessionUpdate(RoutingSessionInfo sessionInfo) {
+ synchronized (mLock) {
+ mSourceSessionInfo = sessionInfo;
+ mTranslatedSessionInfo = asSystemProviderSession(sessionInfo);
+ }
+ notifySessionInfoUpdated();
+ }
+
+ @Override
+ public void onRequestFailed(long requestId, @Reason int reason) {
+ notifyRequestFailed(requestId, reason);
+ }
+
+ @Override
+ public void onSessionReleased() {
+ synchronized (mLock) {
+ removeSelfFromSessionMap();
+ }
+ notifySessionInfoUpdated();
+ }
+
+ @GuardedBy("SystemMediaRoute2Provider2.this.mLock")
+ @Nullable
+ public ProviderProxyRecord getProxyRecord() {
+ ProviderProxyRecord provider = mProxyRecords.get(mProviderId);
+ if (provider == null) {
+ // Unexpected condition where the proxy is no longer available while there's an
+ // ongoing session. Could happen due to a crash in the provider process.
+ removeSelfFromSessionMap();
+ }
+ return provider;
+ }
+
+ @GuardedBy("SystemMediaRoute2Provider2.this.mLock")
+ private void removeSelfFromSessionMap() {
+ mPackageNameToSessionRecord.remove(mSourceSessionInfo.getClientPackageName());
+ }
+
+ private RoutingSessionInfo asSystemProviderSession(RoutingSessionInfo session) {
+ var builder =
+ new RoutingSessionInfo.Builder(session)
+ .setProviderId(mUniqueId)
+ .clearSelectedRoutes()
+ .clearSelectableRoutes()
+ .clearDeselectableRoutes()
+ .clearTransferableRoutes();
+ session.getSelectedRoutes().stream()
+ .map(it -> uniqueIdAsSystemRouteId(session.getProviderId(), it))
+ .forEach(builder::addSelectedRoute);
+ session.getSelectableRoutes().stream()
+ .map(it -> uniqueIdAsSystemRouteId(session.getProviderId(), it))
+ .forEach(builder::addSelectableRoute);
+ session.getDeselectableRoutes().stream()
+ .map(it -> uniqueIdAsSystemRouteId(session.getProviderId(), it))
+ .forEach(builder::addDeselectableRoute);
+ session.getTransferableRoutes().stream()
+ .map(it -> uniqueIdAsSystemRouteId(session.getProviderId(), it))
+ .forEach(builder::addTransferableRoute);
+ return builder.build();
}
}
}