summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Santiago Seifert <aquilescanta@google.com> 2024-12-17 10:11:12 -0800
committer Android (Google) Code Review <android-gerrit@google.com> 2024-12-17 10:11:12 -0800
commitb46969e3b9562d1de20cf894c1268bfe8d190dda (patch)
treefead2df6813432f3e02dfa1f51c4f25fa2d1baf2
parent85772154f2fa1f804ba9e34e180c4dedb5611491 (diff)
parentc9617d35ea4e0323d3fb639a9d734072607e24f4 (diff)
Merge "Implement baseline system media session management" into main
-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();
}
}
}