diff options
| author | 2024-12-17 10:11:12 -0800 | |
|---|---|---|
| committer | 2024-12-17 10:11:12 -0800 | |
| commit | b46969e3b9562d1de20cf894c1268bfe8d190dda (patch) | |
| tree | fead2df6813432f3e02dfa1f51c4f25fa2d1baf2 | |
| parent | 85772154f2fa1f804ba9e34e180c4dedb5611491 (diff) | |
| parent | c9617d35ea4e0323d3fb639a9d734072607e24f4 (diff) | |
Merge "Implement baseline system media session management" into main
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(); } } } |