diff options
16 files changed, 619 insertions, 196 deletions
diff --git a/media/java/android/media/IMediaRoute2Provider.aidl b/media/java/android/media/IMediaRoute2Provider.aidl index d8fd1ff22c89..a03aa868fd84 100644 --- a/media/java/android/media/IMediaRoute2Provider.aidl +++ b/media/java/android/media/IMediaRoute2Provider.aidl @@ -24,6 +24,8 @@ import android.media.IMediaRoute2ProviderClient; */ oneway interface IMediaRoute2Provider { void setClient(IMediaRoute2ProviderClient client); + void requestCreateSession(String packageName, String routeId, String controlCategory, + int requestId); void requestSelectRoute(String packageName, String id, int seq); void unselectRoute(String packageName, String id); void notifyControlRequestSent(String id, in Intent request); diff --git a/media/java/android/media/IMediaRoute2ProviderClient.aidl b/media/java/android/media/IMediaRoute2ProviderClient.aidl index f4fb7f450fb8..8e63c4ae2b27 100644 --- a/media/java/android/media/IMediaRoute2ProviderClient.aidl +++ b/media/java/android/media/IMediaRoute2ProviderClient.aidl @@ -18,6 +18,7 @@ package android.media; import android.media.MediaRoute2ProviderInfo; import android.media.MediaRoute2Info; +import android.media.RouteSessionInfo; import android.os.Bundle; /** @@ -25,5 +26,8 @@ import android.os.Bundle; */ oneway interface IMediaRoute2ProviderClient { void updateProviderInfo(in MediaRoute2ProviderInfo info); - void notifyRouteSelected(String packageName, String routeId, in Bundle controlHints, int seq); + void notifyRouteSelected(String packageName, String routeId, in @nullable Bundle controlHints, + int seq); + void notifySessionCreated(in @nullable RouteSessionInfo sessionInfo, + in @nullable Bundle controlHints, int requestId); } diff --git a/media/java/android/media/IMediaRouter2Client.aidl b/media/java/android/media/IMediaRouter2Client.aidl index b04af7d1d28d..f4c6b6a7c5c4 100644 --- a/media/java/android/media/IMediaRouter2Client.aidl +++ b/media/java/android/media/IMediaRouter2Client.aidl @@ -17,6 +17,7 @@ package android.media; import android.media.MediaRoute2Info; +import android.media.RouteSessionInfo; import android.os.Bundle; /** @@ -27,5 +28,6 @@ oneway interface IMediaRouter2Client { void notifyRoutesAdded(in List<MediaRoute2Info> routes); void notifyRoutesRemoved(in List<MediaRoute2Info> routes); void notifyRoutesChanged(in List<MediaRoute2Info> routes); - void notifyRouteSelected(in MediaRoute2Info route, int reason, in Bundle controlHints); + void notifySessionCreated(in @nullable RouteSessionInfo sessionInfo, + in @nullable Bundle controlHints, int requestId); } diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl index d803f04839b1..faf25631a167 100644 --- a/media/java/android/media/IMediaRouterService.aidl +++ b/media/java/android/media/IMediaRouterService.aidl @@ -48,13 +48,9 @@ interface IMediaRouterService { void sendControlRequest(IMediaRouter2Client client, in MediaRoute2Info route, in Intent request); void requestSetVolume2(IMediaRouter2Client client, in MediaRoute2Info route, int volume); void requestUpdateVolume2(IMediaRouter2Client client, in MediaRoute2Info route, int direction); - /** - * Changes the selected route of the client. - * - * @param client the client that changes it's selected route - * @param route the route to be selected - */ - void requestSelectRoute2(IMediaRouter2Client client, in @nullable MediaRoute2Info route); + + void requestCreateSession(IMediaRouter2Client client, in MediaRoute2Info route, + String controlCategory, int requestId); void setControlCategories(IMediaRouter2Client client, in List<String> categories); void registerManager(IMediaRouter2Manager manager, String packageName); diff --git a/media/java/android/media/MediaRoute2ProviderService.java b/media/java/android/media/MediaRoute2ProviderService.java index 8d6f2551f9f6..6e9f3d0efe87 100644 --- a/media/java/android/media/MediaRoute2ProviderService.java +++ b/media/java/android/media/MediaRoute2ProviderService.java @@ -47,6 +47,7 @@ public abstract class MediaRoute2ProviderService extends Service { private final Handler mHandler; private final Object mSessionLock = new Object(); private ProviderStub mStub; + // TODO: Rename this to mService (and accordingly IMediaRoute2ProviderClient to something else) private IMediaRoute2ProviderClient mClient; private MediaRoute2ProviderInfo mProviderInfo; @@ -166,19 +167,29 @@ public abstract class MediaRoute2ProviderService extends Service { * Notifies clients of that the session is created and ready for use. If the session can be * controlled, pass a {@link Bundle} that contains how to control it. * - * @param sessionId id of the session * @param sessionInfo information of the new session. * Pass {@code null} to reject the request or inform clients that * session creation has failed. * @param controlHints a {@link Bundle} that contains how to control the session. + * @param requestId id of the previous request to create this session */ //TODO: fail reason? - public final void notifySessionCreated(int sessionId, @Nullable RouteSessionInfo sessionInfo, - @Nullable Bundle controlHints) { - //TODO: validate sessionId (it must be in "waiting list") - synchronized (mSessionLock) { - mSessionInfo.put(sessionId, sessionInfo); - //TODO: notify media router service of session creation. + public final void notifySessionCreated(@Nullable RouteSessionInfo sessionInfo, + @Nullable Bundle controlHints, int requestId) { + //TODO: validate sessionInfo.getSessionId() (it must be in "waiting list") + if (sessionInfo != null) { + synchronized (mSessionLock) { + mSessionInfo.put(sessionInfo.getSessionId(), sessionInfo); + } + } + + if (mClient == null) { + return; + } + try { + mClient.notifySessionCreated(sessionInfo, controlHints, requestId); + } catch (RemoteException ex) { + Log.w(TAG, "Failed to notify session created."); } } @@ -203,18 +214,19 @@ public abstract class MediaRoute2ProviderService extends Service { /** * Called when a session should be created. * You should create and maintain your own session and notifies the client of - * session info. Call {@link #notifySessionCreated(int, RouteSessionInfo, Bundle)} - * to notify the information of a new session. + * session info. Call {@link #notifySessionCreated(RouteSessionInfo, Bundle, int)} + * with the given {@code requestId} to notify the information of a new session. * If you can't create the session or want to reject the request, pass {@code null} - * as session info in {@link #notifySessionCreated(int, RouteSessionInfo, Bundle)}. + * as session info in {@link #notifySessionCreated(RouteSessionInfo, Bundle, int)} + * with the given {@code requestId}. * * @param packageName the package name of the application that selected the route * @param routeId the id of the route initially being connected * @param controlCategory the control category of the new session - * @param sessionId the id of a new session + * @param requestId the id of this session creation request */ public abstract void onCreateSession(@NonNull String packageName, @NonNull String routeId, - @NonNull String controlCategory, int sessionId); + @NonNull String controlCategory, int requestId); /** * Called when a session is about to be destroyed. @@ -339,6 +351,14 @@ public abstract class MediaRoute2ProviderService extends Service { } @Override + public void requestCreateSession(String packageName, String routeId, String controlCategory, + int requestId) { + mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onCreateSession, + MediaRoute2ProviderService.this, packageName, routeId, controlCategory, + requestId)); + } + + @Override public void requestSelectRoute(String packageName, String routeId, int seq) { //TODO: call onCreateSession instead mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onSelectRoute, diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index f9dabcf8c90f..161a75a9679d 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -29,6 +29,7 @@ import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.text.TextUtils; @@ -46,6 +47,7 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; /** * A new Media Router @@ -98,7 +100,10 @@ public class MediaRouter2 { private final Context mContext; private final IMediaRouterService mMediaRouterService; - private final CopyOnWriteArrayList<CallbackRecord> mCallbackRecords = + private final CopyOnWriteArrayList<RouteCallbackRecord> mRouteCallbackRecords = + new CopyOnWriteArrayList<>(); + + private final CopyOnWriteArrayList<SessionCreationRequest> mSessionCreationRequests = new CopyOnWriteArrayList<>(); private final String mPackageName; @@ -108,12 +113,12 @@ public class MediaRouter2 { @GuardedBy("sLock") private List<String> mControlCategories = Collections.emptyList(); - private MediaRoute2Info mSelectedRoute; - @GuardedBy("sLock") - private MediaRoute2Info mSelectingRoute; + // TODO: Make MediaRouter2 is always connected to the MediaRouterService. @GuardedBy("sLock") private Client2 mClient; + private AtomicInteger mSessionCreationRequestCnt = new AtomicInteger(1); + final Handler mHandler; @GuardedBy("sLock") private boolean mShouldUpdateRoutes; @@ -154,18 +159,14 @@ public class MediaRouter2 { for (MediaRoute2Info route : currentSystemRoutes) { mRoutes.put(route.getId(), route); } - // The first route is the currently selected system route. - // For example, if there are two system routes (BT and device speaker), - // BT will be the first route in the list. - mSelectedRoute = currentSystemRoutes.get(0); } /** * Registers a callback to discover routes and to receive events when they change. */ public void registerCallback(@NonNull @CallbackExecutor Executor executor, - @NonNull Callback callback) { - registerCallback(executor, callback, 0); + @NonNull RouteCallback routeCallback) { + registerCallback(executor, routeCallback, 0); } /** @@ -175,12 +176,12 @@ public class MediaRouter2 { * </p> */ public void registerCallback(@NonNull @CallbackExecutor Executor executor, - @NonNull Callback callback, int flags) { + @NonNull RouteCallback routeCallback, int flags) { Objects.requireNonNull(executor, "executor must not be null"); - Objects.requireNonNull(callback, "callback must not be null"); + Objects.requireNonNull(routeCallback, "callback must not be null"); - CallbackRecord record = new CallbackRecord(callback, executor, flags); - if (!mCallbackRecords.addIfAbsent(record)) { + RouteCallbackRecord record = new RouteCallbackRecord(executor, routeCallback, flags); + if (!mRouteCallbackRecords.addIfAbsent(record)) { Log.w(TAG, "Ignoring the same callback"); return; } @@ -205,19 +206,20 @@ public class MediaRouter2 { * Unregisters the given callback. The callback will no longer receive events. * If the callback has not been added or been removed already, it is ignored. * - * @param callback the callback to unregister + * @param routeCallback the callback to unregister * @see #registerCallback */ - public void unregisterCallback(@NonNull Callback callback) { - Objects.requireNonNull(callback, "callback must not be null"); + public void unregisterCallback(@NonNull RouteCallback routeCallback) { + Objects.requireNonNull(routeCallback, "callback must not be null"); - if (!mCallbackRecords.remove(new CallbackRecord(callback, null, 0))) { + if (!mRouteCallbackRecords.remove( + new RouteCallbackRecord(null, routeCallback, 0))) { Log.w(TAG, "Ignoring unknown callback"); return; } synchronized (sLock) { - if (mCallbackRecords.size() == 0 && mClient != null) { + if (mRouteCallbackRecords.size() == 0 && mClient != null) { try { mMediaRouterService.unregisterClient2(mClient); } catch (RemoteException ex) { @@ -282,38 +284,51 @@ public class MediaRouter2 { } /** - * Gets the currently selected route. + * Requests the media route provider service to create a session with the given route. * - * @return the selected route - */ - @NonNull - public MediaRoute2Info getSelectedRoute() { - return mSelectedRoute; - } - - /** - * Request to select the specified route. When the route is selected, - * {@link Callback#onRouteSelected(MediaRoute2Info, int, Bundle)} will be called. + * @param route the route you want to create a session with. + * @param controlCategory the control category of the session. Should not be empty + * @param executor the executor to get the result of the session creation + * @param callback the callback to get the result of the session creation * - * @param route the route to select + * @see SessionCreationCallback#onSessionCreated(RouteSessionController, Bundle) + * @see SessionCreationCallback#onSessionCreationFailed() */ - public void requestSelectRoute(@NonNull MediaRoute2Info route) { + @NonNull + public void requestCreateSession(@NonNull MediaRoute2Info route, + @NonNull String controlCategory, + @CallbackExecutor Executor executor, @NonNull SessionCreationCallback callback) { Objects.requireNonNull(route, "route must not be null"); + if (TextUtils.isEmpty(controlCategory)) { + throw new IllegalArgumentException("controlCategory must not be empty"); + } + Objects.requireNonNull(executor, "executor must not be null"); + Objects.requireNonNull(callback, "callback must not be null"); + + // TODO: Check the given route exists + // TODO: Check the route supports the given controlCategory + + final int requestId; + // TODO: This does not ensure the uniqueness of the request ID. + // Find the way to ensure it. (e.g. have mapping inside MediaRouterService) + requestId = Process.myPid() * 10000 + mSessionCreationRequestCnt.getAndIncrement(); + + SessionCreationRequest request = new SessionCreationRequest( + requestId, route, controlCategory, executor, callback); + mSessionCreationRequests.add(request); Client2 client; synchronized (sLock) { - if (mSelectingRoute == route) { - Log.w(TAG, "The route selection request is already sent."); - return; - } - mSelectingRoute = route; client = mClient; } if (client != null) { try { - mMediaRouterService.requestSelectRoute2(client, route); + mMediaRouterService.requestCreateSession( + client, route, controlCategory, requestId); } catch (RemoteException ex) { - Log.e(TAG, "Unable to request to select route.", ex); + Log.e(TAG, "Unable to request to create session.", ex); + mHandler.sendMessage(obtainMessage(MediaRouter2::createControllerOnHandler, + MediaRouter2.this, null, null, requestId)); } } } @@ -472,57 +487,71 @@ public class MediaRouter2 { } } - void selectRouteOnHandler(MediaRoute2Info route, int reason, Bundle controlHints) { - synchronized (sLock) { - if (reason == SELECT_REASON_USER_SELECTED) { - if (mSelectingRoute == null - || !TextUtils.equals(mSelectingRoute.getUniqueId(), route.getUniqueId())) { - Log.w(TAG, "Ignoring invalid or outdated notifyRouteSelected call. " - + "selectingRoute=" + mSelectingRoute + " route=" + route); - return; - } + /** + * Creates a controller and calls the {@link SessionCreationCallback#onSessionCreated}. + * If session creation has failed, then it calls + * {@link SessionCreationCallback#onSessionCreationFailed()}. + * <p> + * Pass {@code null} to sessionInfo for the failure case. + */ + void createControllerOnHandler(@Nullable RouteSessionInfo sessionInfo, + @Nullable Bundle controlHints, int requestId) { + SessionCreationRequest matchingRequest = null; + for (SessionCreationRequest request : mSessionCreationRequests) { + if (request.mRequestId == requestId) { + matchingRequest = request; + break; } - mSelectingRoute = null; } - if (reason == SELECT_REASON_SYSTEM_SELECTED) { - reason = SELECT_REASON_USER_SELECTED; + + if (matchingRequest == null) { + Log.w(TAG, "Ignoring session creation result for unknown request." + + " requestId=" + requestId + ", sessionInfo=" + sessionInfo); + return; + } + + mSessionCreationRequests.remove(matchingRequest); + + final Executor executor = matchingRequest.mExecutor; + final SessionCreationCallback callback = matchingRequest.mSessionCreationCallback; + + if (sessionInfo == null) { + // TODO: We may need to distinguish between failure and rejection. + // One way can be introducing 'reason'. + executor.execute(callback::onSessionCreationFailed); + } else { + // TODO: RouteSessionController should be created with full info (e.g. routes) + // from RouteSessionInfo. + RouteSessionController controller = new RouteSessionController(sessionInfo); + executor.execute(() -> callback.onSessionCreated(controller, controlHints)); } - mSelectedRoute = route; - notifyRouteSelected(route, reason, controlHints); } private void notifyRoutesAdded(List<MediaRoute2Info> routes) { - for (CallbackRecord record: mCallbackRecords) { + for (RouteCallbackRecord record: mRouteCallbackRecords) { record.mExecutor.execute( - () -> record.mCallback.onRoutesAdded(routes)); + () -> record.mRouteCallback.onRoutesAdded(routes)); } } private void notifyRoutesRemoved(List<MediaRoute2Info> routes) { - for (CallbackRecord record: mCallbackRecords) { + for (RouteCallbackRecord record: mRouteCallbackRecords) { record.mExecutor.execute( - () -> record.mCallback.onRoutesRemoved(routes)); + () -> record.mRouteCallback.onRoutesRemoved(routes)); } } private void notifyRoutesChanged(List<MediaRoute2Info> routes) { - for (CallbackRecord record: mCallbackRecords) { - record.mExecutor.execute( - () -> record.mCallback.onRoutesChanged(routes)); - } - } - - private void notifyRouteSelected(MediaRoute2Info route, int reason, Bundle controlHints) { - for (CallbackRecord record: mCallbackRecords) { + for (RouteCallbackRecord record: mRouteCallbackRecords) { record.mExecutor.execute( - () -> record.mCallback.onRouteSelected(route, reason, controlHints)); + () -> record.mRouteCallback.onRoutesChanged(routes)); } } /** - * Interface for receiving events about media routing changes. + * Callback for receiving events about media route discovery. */ - public static class Callback { + public static class RouteCallback { /** * Called when routes are added. Whenever you registers a callback, this will * be invoked with known routes. @@ -549,30 +578,33 @@ public class MediaRouter2 { * @param routes the list of routes that have been changed. It's never empty. */ public void onRoutesChanged(@NonNull List<MediaRoute2Info> routes) {} + } + /** + * Callback for receiving a result of session creation. + */ + public static class SessionCreationCallback { /** - * Called when a route is selected. Exactly one route can be selected at a time. + * Called when the route session is created by the route provider. * - * @param route the selected route. - * @param reason the reason why the route is selected. - * @param controlHints An optional bundle of provider-specific arguments which may be - * used to control the selected route. Can be empty. - * @see #SELECT_REASON_UNKNOWN - * @see #SELECT_REASON_USER_SELECTED - * @see #SELECT_REASON_FALLBACK - * @see #getSelectedRoute() + * @param controller the controller to control the created session */ - public void onRouteSelected(@NonNull MediaRoute2Info route, @SelectReason int reason, - @NonNull Bundle controlHints) {} + public void onSessionCreated(RouteSessionController controller, Bundle controlHints) {} + + /** + * Called when the session creation request failed. + */ + public void onSessionCreationFailed() {} } - final class CallbackRecord { - public final Callback mCallback; - public Executor mExecutor; - public int mFlags; + final class RouteCallbackRecord { + public final Executor mExecutor; + public final RouteCallback mRouteCallback; + public final int mFlags; - CallbackRecord(@NonNull Callback callback, @Nullable Executor executor, int flags) { - mCallback = callback; + RouteCallbackRecord(@Nullable Executor executor, @NonNull RouteCallback routeCallback, + int flags) { + mRouteCallback = routeCallback; mExecutor = executor; mFlags = flags; } @@ -582,15 +614,35 @@ public class MediaRouter2 { if (this == obj) { return true; } - if (!(obj instanceof CallbackRecord)) { + if (!(obj instanceof RouteCallbackRecord)) { return false; } - return mCallback == ((CallbackRecord) obj).mCallback; + return mRouteCallback + == ((RouteCallbackRecord) obj).mRouteCallback; } @Override public int hashCode() { - return mCallback.hashCode(); + return mRouteCallback.hashCode(); + } + } + + final class SessionCreationRequest { + public final MediaRoute2Info mRoute; + public final String mControlCategory; + public final Executor mExecutor; + public final SessionCreationCallback mSessionCreationCallback; + public final int mRequestId; + + SessionCreationRequest(int requestId, @NonNull MediaRoute2Info route, + @NonNull String controlCategory, + @Nullable Executor executor, + @NonNull SessionCreationCallback sessionCreationCallback) { + mRoute = route; + mControlCategory = controlCategory; + mExecutor = executor; + mSessionCreationCallback = sessionCreationCallback; + mRequestId = requestId; } } @@ -617,10 +669,10 @@ public class MediaRouter2 { } @Override - public void notifyRouteSelected(MediaRoute2Info route, int reason, - Bundle controlHints) { - mHandler.sendMessage(obtainMessage(MediaRouter2::selectRouteOnHandler, - MediaRouter2.this, route, reason, controlHints)); + public void notifySessionCreated(@Nullable RouteSessionInfo sessionInfo, + @Nullable Bundle controlHints, int requestId) { + mHandler.sendMessage(obtainMessage(MediaRouter2::createControllerOnHandler, + MediaRouter2.this, sessionInfo, controlHints, requestId)); } } } diff --git a/media/java/android/media/RouteSessionController.java b/media/java/android/media/RouteSessionController.java index 5ff721837573..b6e8bb55aa4c 100644 --- a/media/java/android/media/RouteSessionController.java +++ b/media/java/android/media/RouteSessionController.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import com.android.internal.annotations.GuardedBy; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; @@ -39,6 +40,8 @@ public class RouteSessionController { private final String mCategory; private final Object mLock = new Object(); + private List<String> mSelectedRoutes; + @GuardedBy("mLock") private final CopyOnWriteArrayList<CallbackRecord> mCallbackRecords = new CopyOnWriteArrayList<>(); @@ -46,12 +49,13 @@ public class RouteSessionController { private volatile boolean mIsReleased; /** - * @param sessionId the ID of the session. - * @param category The category of media routes that the session includes. + * @param sessionInfo */ - RouteSessionController(int sessionId, @NonNull String category) { - mSessionId = sessionId; - mCategory = category; + RouteSessionController(@NonNull RouteSessionInfo sessionInfo) { + mSessionId = sessionInfo.getSessionId(); + mCategory = sessionInfo.getControlCategory(); + mSelectedRoutes = sessionInfo.getSelectedRoutes(); + // TODO: Create getters for all other types of routes } /** @@ -70,12 +74,11 @@ public class RouteSessionController { } /** - * @return the list of currently connected routes + * @return the list of currently selected routes */ @NonNull - public List<MediaRoute2Info> getRoutes() { - // TODO: Implement this when SessionInfo is introduced. - return null; + public List<String> getSelectedRoutes() { + return Collections.unmodifiableList(mSelectedRoutes); } /** @@ -93,7 +96,7 @@ public class RouteSessionController { /** * Add routes to the remote session. * - * @see #getRoutes() + * @see #getSelectedRoutes() * @see Callback#onSessionInfoChanged */ public void addRoutes(List<MediaRoute2Info> routes) { @@ -102,9 +105,10 @@ public class RouteSessionController { /** * Remove routes from this session. Media may be stopped on those devices. - * Route removal requests that are not currently in {@link #getRoutes()} will be ignored. + * Route removal requests that are not currently in {@link #getSelectedRoutes()} will be + * ignored. * - * @see #getRoutes() + * @see #getSelectedRoutes() * @see Callback#onSessionInfoChanged */ public void removeRoutes(List<MediaRoute2Info> routes) { diff --git a/media/tests/MediaRouteProvider/src/com/android/mediarouteprovider/example/SampleMediaRoute2ProviderService.java b/media/tests/MediaRouteProvider/src/com/android/mediarouteprovider/example/SampleMediaRoute2ProviderService.java index 524be8c014c2..98efeb82cea3 100644 --- a/media/tests/MediaRouteProvider/src/com/android/mediarouteprovider/example/SampleMediaRoute2ProviderService.java +++ b/media/tests/MediaRouteProvider/src/com/android/mediarouteprovider/example/SampleMediaRoute2ProviderService.java @@ -26,6 +26,7 @@ import android.media.MediaRoute2ProviderService; import android.media.RouteSessionInfo; import android.os.Bundle; import android.os.IBinder; +import android.text.TextUtils; import java.util.HashMap; import java.util.Map; @@ -37,6 +38,9 @@ public class SampleMediaRoute2ProviderService extends MediaRoute2ProviderService public static final String ROUTE_NAME1 = "Sample Route 1"; public static final String ROUTE_ID2 = "route_id2"; public static final String ROUTE_NAME2 = "Sample Route 2"; + public static final String ROUTE_ID3_SESSION_CREATION_FAILED = + "route_id3_session_creation_failed"; + public static final String ROUTE_NAME3 = "Sample Route 3 - Session creation failed"; public static final String ROUTE_ID_SPECIAL_CATEGORY = "route_special_category"; public static final String ROUTE_NAME_SPECIAL_CATEGORY = "Special Category Route"; @@ -55,6 +59,8 @@ public class SampleMediaRoute2ProviderService extends MediaRoute2ProviderService public static final String CATEGORY_SPECIAL = "com.android.mediarouteprovider.CATEGORY_SPECIAL"; + public static final int SESSION_ID_1 = 1000; + Map<String, MediaRoute2Info> mRoutes = new HashMap<>(); private void initializeRoutes() { @@ -66,6 +72,10 @@ public class SampleMediaRoute2ProviderService extends MediaRoute2ProviderService .addSupportedCategory(CATEGORY_SAMPLE) .setDeviceType(DEVICE_TYPE_SPEAKER) .build(); + MediaRoute2Info route3 = new MediaRoute2Info.Builder( + ROUTE_ID3_SESSION_CREATION_FAILED, ROUTE_NAME3) + .addSupportedCategory(CATEGORY_SAMPLE) + .build(); MediaRoute2Info routeSpecial = new MediaRoute2Info.Builder(ROUTE_ID_SPECIAL_CATEGORY, ROUTE_NAME_SPECIAL_CATEGORY) .addSupportedCategory(CATEGORY_SAMPLE) @@ -85,6 +95,7 @@ public class SampleMediaRoute2ProviderService extends MediaRoute2ProviderService mRoutes.put(route1.getId(), route1); mRoutes.put(route2.getId(), route2); + mRoutes.put(route3.getId(), route3); mRoutes.put(routeSpecial.getId(), routeSpecial); mRoutes.put(fixedVolumeRoute.getId(), fixedVolumeRoute); mRoutes.put(variableVolumeRoute.getId(), variableVolumeRoute); @@ -168,12 +179,18 @@ public class SampleMediaRoute2ProviderService extends MediaRoute2ProviderService @Override public void onCreateSession(String packageName, String routeId, String controlCategory, - int sessionId) { + int requestId) { + if (TextUtils.equals(ROUTE_ID3_SESSION_CREATION_FAILED, routeId)) { + // Tell the router that session cannot be created by passing null as sessionInfo. + notifySessionCreated(null /* sessionInfo */, null /* controlHitns */, requestId); + return; + } + RouteSessionInfo sessionInfo = new RouteSessionInfo.Builder( - sessionId, packageName, controlCategory) + SESSION_ID_1, packageName, controlCategory) .addSelectedRoute(routeId) .build(); - notifySessionCreated(sessionId, sessionInfo, null); + notifySessionCreated(sessionInfo, null /* controlHints */, requestId); } @Override diff --git a/media/tests/MediaRouter/Android.bp b/media/tests/MediaRouter/Android.bp index 611b25a2f128..5a0a50c2ae38 100644 --- a/media/tests/MediaRouter/Android.bp +++ b/media/tests/MediaRouter/Android.bp @@ -11,6 +11,7 @@ android_test { static_libs: [ "android-support-test", "mockito-target-minus-junit4", + "testng" ], platform_apis: true, diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2Test.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2Test.java index 6f1a070d6f7c..e093599e5a0a 100644 --- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2Test.java +++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2Test.java @@ -27,19 +27,26 @@ import static com.android.mediaroutertest.MediaRouterManagerTest.CATEGORIES_ALL; import static com.android.mediaroutertest.MediaRouterManagerTest.CATEGORIES_SPECIAL; import static com.android.mediaroutertest.MediaRouterManagerTest.CATEGORY_SAMPLE; import static com.android.mediaroutertest.MediaRouterManagerTest.CATEGORY_SPECIAL; +import static com.android.mediaroutertest.MediaRouterManagerTest.ROUTE_ID1; +import static com.android.mediaroutertest.MediaRouterManagerTest.ROUTE_ID3_SESSION_CREATION_FAILED; import static com.android.mediaroutertest.MediaRouterManagerTest.ROUTE_ID_SPECIAL_CATEGORY; import static com.android.mediaroutertest.MediaRouterManagerTest.ROUTE_ID_VARIABLE_VOLUME; import static com.android.mediaroutertest.MediaRouterManagerTest.SYSTEM_PROVIDER_ID; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.testng.Assert.assertThrows; import android.content.Context; import android.media.MediaRoute2Info; import android.media.MediaRouter2; +import android.media.MediaRouter2.SessionCreationCallback; +import android.media.RouteSessionController; import android.net.Uri; +import android.os.Bundle; import android.os.Parcel; import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; @@ -51,6 +58,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -81,13 +89,6 @@ public class MediaRouter2Test { public void tearDown() throws Exception { } - @Test - public void testGetSelectedRoute_afterCreation() throws Exception { - MediaRouter2 router = MediaRouter2.getInstance(mContext); - MediaRoute2Info initiallySelectedRoute = router.getSelectedRoute(); - assertNotNull(initiallySelectedRoute); - } - /** * Tests if we get proper routes for application that has special control category. */ @@ -204,6 +205,109 @@ public class MediaRouter2Test { (route -> route.getVolume() == originalVolume)); } + @Test + public void testRequestCreateSessionWithInvalidArguments() { + MediaRoute2Info route = new MediaRoute2Info.Builder("id", "name").build(); + String controlCategory = "controlCategory"; + Executor executor = mExecutor; + MediaRouter2.SessionCreationCallback callback = new MediaRouter2.SessionCreationCallback(); + + // Tests null route + assertThrows(NullPointerException.class, + () -> mRouter2.requestCreateSession(null, controlCategory, executor, callback)); + + // Tests null or empty control category + assertThrows(IllegalArgumentException.class, + () -> mRouter2.requestCreateSession(route, null, executor, callback)); + assertThrows(IllegalArgumentException.class, + () -> mRouter2.requestCreateSession(route, "", executor, callback)); + + // Tests null executor + assertThrows(NullPointerException.class, + () -> mRouter2.requestCreateSession(route, controlCategory, null, callback)); + + // Tests null callback + assertThrows(NullPointerException.class, + () -> mRouter2.requestCreateSession(route, controlCategory, executor, null)); + } + + @Test + public void testRequestCreateSessionSuccess() throws Exception { + final List<String> sampleControlCategory = new ArrayList<>(); + sampleControlCategory.add(CATEGORY_SAMPLE); + + Map<String, MediaRoute2Info> routes = waitAndGetRoutes(sampleControlCategory); + MediaRoute2Info route = routes.get(ROUTE_ID1); + assertNotNull(route); + + final CountDownLatch successLatch = new CountDownLatch(1); + final CountDownLatch failureLatch = new CountDownLatch(1); + + // Create session with this route + SessionCreationCallback callback = new SessionCreationCallback() { + @Override + public void onSessionCreated(RouteSessionController controller, Bundle controlHints) { + assertNotNull(controller); + assertTrue(controller.getSelectedRoutes().contains(ROUTE_ID1)); + assertTrue(TextUtils.equals(CATEGORY_SAMPLE, controller.getCategory())); + successLatch.countDown(); + } + + @Override + public void onSessionCreationFailed() { + failureLatch.countDown(); + } + }; + + // TODO: Remove this once the MediaRouter2 becomes always connected to the service. + mRouter2.registerCallback(mExecutor, new MediaRouter2.RouteCallback()); + + mRouter2.requestCreateSession(route, CATEGORY_SAMPLE, mExecutor, callback); + assertTrue(successLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + + // onSessionCreationFailed should not be called. + assertFalse(failureLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + } + + @Test + public void testRequestCreateSessionFailure() throws Exception { + final List<String> sampleControlCategory = new ArrayList<>(); + sampleControlCategory.add(CATEGORY_SAMPLE); + + Map<String, MediaRoute2Info> routes = waitAndGetRoutes(sampleControlCategory); + MediaRoute2Info route = routes.get(ROUTE_ID3_SESSION_CREATION_FAILED); + assertNotNull(route); + + final CountDownLatch successLatch = new CountDownLatch(1); + final CountDownLatch failureLatch = new CountDownLatch(1); + + // Create session with this route + SessionCreationCallback callback = new SessionCreationCallback() { + @Override + public void onSessionCreated(RouteSessionController controller, Bundle controlHints) { + successLatch.countDown(); + } + + @Override + public void onSessionCreationFailed() { + failureLatch.countDown(); + } + }; + + // TODO: Remove this once the MediaRouter2 becomes always connected to the service. + mRouter2.registerCallback(mExecutor, new MediaRouter2.RouteCallback()); + + mRouter2.requestCreateSession(route, CATEGORY_SAMPLE, mExecutor, callback); + assertTrue(failureLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + + // onSessionCreated should not be called. + assertFalse(successLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + } + + @Test + public void testRequestCreateSessionMultipleSessions() throws Exception { + // TODO: Test creating multiple sessions (Check the ID of each controller) + } // Helper for getting routes easily static Map<String, MediaRoute2Info> createRouteMap(List<MediaRoute2Info> routes) { @@ -220,7 +324,8 @@ public class MediaRouter2Test { CountDownLatch latch = new CountDownLatch(1); // A dummy callback is required to send control category info. - MediaRouter2.Callback routerCallback = new MediaRouter2.Callback() { + MediaRouter2.RouteCallback + routeCallback = new MediaRouter2.RouteCallback() { @Override public void onRoutesAdded(List<MediaRoute2Info> routes) { for (int i = 0; i < routes.size(); i++) { @@ -233,19 +338,20 @@ public class MediaRouter2Test { }; mRouter2.setControlCategories(controlCategories); - mRouter2.registerCallback(mExecutor, routerCallback); + mRouter2.registerCallback(mExecutor, routeCallback); try { latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS); return createRouteMap(mRouter2.getRoutes()); } finally { - mRouter2.unregisterCallback(routerCallback); + mRouter2.unregisterCallback(routeCallback); } } void awaitOnRouteChanged(Runnable task, String routeId, Predicate<MediaRoute2Info> predicate) throws Exception { CountDownLatch latch = new CountDownLatch(1); - MediaRouter2.Callback callback = new MediaRouter2.Callback() { + MediaRouter2.RouteCallback + routeCallback = new MediaRouter2.RouteCallback() { @Override public void onRoutesChanged(List<MediaRoute2Info> changed) { MediaRoute2Info route = createRouteMap(changed).get(routeId); @@ -254,12 +360,12 @@ public class MediaRouter2Test { } } }; - mRouter2.registerCallback(mExecutor, callback); + mRouter2.registerCallback(mExecutor, routeCallback); try { task.run(); assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); } finally { - mRouter2.unregisterCallback(callback); + mRouter2.unregisterCallback(routeCallback); } } } diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java index c5d8a96c7e08..4b3dc0d184a4 100644 --- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java +++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java @@ -20,6 +20,7 @@ import static android.media.MediaRoute2Info.PLAYBACK_VOLUME_FIXED; import static android.media.MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -27,8 +28,8 @@ import android.content.Context; import android.content.Intent; import android.media.MediaRoute2Info; import android.media.MediaRouter2; +import android.media.MediaRouter2.RouteCallback; import android.media.MediaRouter2Manager; -import android.os.Bundle; import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; @@ -59,6 +60,9 @@ public class MediaRouterManagerTest { public static final String ROUTE_NAME1 = "Sample Route 1"; public static final String ROUTE_ID2 = "route_id2"; public static final String ROUTE_NAME2 = "Sample Route 2"; + public static final String ROUTE_ID3_SESSION_CREATION_FAILED = + "route_id3_session_creation_failed"; + public static final String ROUTE_NAME3 = "Sample Route 3 - Session creation failed"; public static final String ROUTE_ID_SPECIAL_CATEGORY = "route_special_category"; public static final String ROUTE_NAME_SPECIAL_CATEGORY = "Special Category Route"; @@ -91,7 +95,8 @@ public class MediaRouterManagerTest { private String mPackageName; private final List<MediaRouter2Manager.Callback> mManagerCallbacks = new ArrayList<>(); - private final List<MediaRouter2.Callback> mRouterCallbacks = new ArrayList<>(); + private final List<RouteCallback> mRouteCallbacks = + new ArrayList<>(); public static final List<String> CATEGORIES_ALL = new ArrayList(); public static final List<String> CATEGORIES_SPECIAL = new ArrayList(); @@ -123,7 +128,21 @@ public class MediaRouterManagerTest { clearCallbacks(); } - /** + //TODO: Move to a separate file + @Test + public void testMediaRoute2Info() { + MediaRoute2Info routeInfo1 = new MediaRoute2Info.Builder("id", "name") + .build(); + MediaRoute2Info routeInfo2 = new MediaRoute2Info.Builder(routeInfo1).build(); + + MediaRoute2Info routeInfo3 = new MediaRoute2Info.Builder(routeInfo1) + .setClientPackageName(mPackageName).build(); + + assertEquals(routeInfo1, routeInfo2); + assertNotEquals(routeInfo1, routeInfo3); + } + + /** * Tests if routes are added correctly when a new callback is registered. */ @Test @@ -149,7 +168,7 @@ public class MediaRouterManagerTest { CountDownLatch latch = new CountDownLatch(1); Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(CATEGORIES_ALL); - addRouterCallback(new MediaRouter2.Callback()); + addRouterCallback(new RouteCallback()); addManagerCallback(new MediaRouter2Manager.Callback() { @Override public void onRoutesRemoved(List<MediaRoute2Info> routes) { @@ -162,6 +181,8 @@ public class MediaRouterManagerTest { } }); + //TODO: Figure out a more proper way to test. + // (Control requests shouldn't be used in this way.) mRouter2.sendControlRequest(routes.get(ROUTE_ID2), new Intent(ACTION_REMOVE_ROUTE)); assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); } @@ -179,29 +200,30 @@ public class MediaRouterManagerTest { /** * Tests if MR2.Callback.onRouteSelected is called when a route is selected from MR2Manager. + * + * TODO: Change this test so that this test check whether the route is added in a session. + * Until then, temporailiy removing @Test annotation. */ - @Test public void testRouterOnRouteSelected() throws Exception { Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(CATEGORIES_ALL); CountDownLatch latch = new CountDownLatch(1); addManagerCallback(new MediaRouter2Manager.Callback()); - addRouterCallback(new MediaRouter2.Callback() { - @Override - public void onRouteSelected(MediaRoute2Info route, int reason, Bundle controlHints) { - if (route != null && TextUtils.equals(route.getId(), ROUTE_ID1)) { - latch.countDown(); - } - } - }); +// addRouterCallback(new RouteDiscoveryCallback() { +// @Override +// public void onRouteSelected(MediaRoute2Info route, int reason, Bundle controlHints) { +// if (route != null && TextUtils.equals(route.getId(), ROUTE_ID1)) { +// latch.countDown(); +// } +// } +// }); MediaRoute2Info routeToSelect = routes.get(ROUTE_ID1); assertNotNull(routeToSelect); try { mManager.selectRoute(mPackageName, routeToSelect); - assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); } finally { mManager.unselectRoute(mPackageName); @@ -217,7 +239,7 @@ public class MediaRouterManagerTest { CountDownLatch latch = new CountDownLatch(1); Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(CATEGORIES_ALL); - addRouterCallback(new MediaRouter2.Callback()); + addRouterCallback(new RouteCallback()); addManagerCallback(new MediaRouter2Manager.Callback() { @Override public void onRouteSelected(String packageName, MediaRoute2Info route) { @@ -245,7 +267,7 @@ public class MediaRouterManagerTest { CountDownLatch latch2 = new CountDownLatch(1); Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(CATEGORIES_ALL); - addRouterCallback(new MediaRouter2.Callback()); + addRouterCallback(new RouteCallback()); addManagerCallback(new MediaRouter2Manager.Callback() { @Override public void onRouteSelected(String packageName, MediaRoute2Info route) { @@ -276,7 +298,7 @@ public class MediaRouterManagerTest { @Test public void testSingleProviderSelect() throws Exception { Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(CATEGORIES_ALL); - addRouterCallback(new MediaRouter2.Callback()); + addRouterCallback(new RouteCallback()); awaitOnRouteChangedManager( () -> mManager.selectRoute(mPackageName, routes.get(ROUTE_ID1)), @@ -330,7 +352,8 @@ public class MediaRouterManagerTest { CountDownLatch latch = new CountDownLatch(2); // A dummy callback is required to send control category info. - MediaRouter2.Callback routerCallback = new MediaRouter2.Callback(); + RouteCallback + routeCallback = new RouteCallback(); MediaRouter2Manager.Callback managerCallback = new MediaRouter2Manager.Callback() { @Override public void onRoutesAdded(List<MediaRoute2Info> routes) { @@ -352,12 +375,12 @@ public class MediaRouterManagerTest { }; mManager.registerCallback(mExecutor, managerCallback); mRouter2.setControlCategories(controlCategories); - mRouter2.registerCallback(mExecutor, routerCallback); + mRouter2.registerCallback(mExecutor, routeCallback); try { latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS); return createRouteMap(mManager.getAvailableRoutes(mPackageName)); } finally { - mRouter2.unregisterCallback(routerCallback); + mRouter2.unregisterCallback(routeCallback); mManager.unregisterCallback(managerCallback); } } @@ -398,9 +421,9 @@ public class MediaRouterManagerTest { mManager.registerCallback(mExecutor, callback); } - private void addRouterCallback(MediaRouter2.Callback callback) { - mRouterCallbacks.add(callback); - mRouter2.registerCallback(mExecutor, callback); + private void addRouterCallback(RouteCallback routeCallback) { + mRouteCallbacks.add(routeCallback); + mRouter2.registerCallback(mExecutor, routeCallback); } private void clearCallbacks() { @@ -409,9 +432,9 @@ public class MediaRouterManagerTest { } mManagerCallbacks.clear(); - for (MediaRouter2.Callback callback : mRouterCallbacks) { - mRouter2.unregisterCallback(callback); + for (RouteCallback routeCallback : mRouteCallbacks) { + mRouter2.unregisterCallback(routeCallback); } - mRouterCallbacks.clear(); + mRouteCallbacks.clear(); } } diff --git a/services/core/java/com/android/server/media/MediaRoute2Provider.java b/services/core/java/com/android/server/media/MediaRoute2Provider.java index 9a49c166e2b2..85509f7d1ee0 100644 --- a/services/core/java/com/android/server/media/MediaRoute2Provider.java +++ b/services/core/java/com/android/server/media/MediaRoute2Provider.java @@ -22,6 +22,7 @@ import android.content.ComponentName; import android.content.Intent; import android.media.MediaRoute2Info; import android.media.MediaRoute2ProviderInfo; +import android.media.RouteSessionInfo; import android.os.Bundle; import java.util.Objects; @@ -42,6 +43,8 @@ abstract class MediaRoute2Provider { mCallback = callback; } + public abstract void requestCreateSession(String packageName, String routeId, + String controlCategory, int requestId); public abstract void requestSelectRoute(String packageName, String routeId, int seq); public abstract void unselectRoute(String packageName, String routeId); public abstract void sendControlRequest(MediaRoute2Info route, Intent request); @@ -82,5 +85,8 @@ abstract class MediaRoute2Provider { void onRouteSelected(@NonNull MediaRoute2ProviderProxy provider, @NonNull String clientPackageName, @NonNull MediaRoute2Info route, @Nullable Bundle controlHints, int seq); + void onSessionCreated(@NonNull MediaRoute2Provider provider, + @Nullable RouteSessionInfo sessionInfo, @Nullable Bundle controlHints, + int requestId); } } diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java index a5abb1835e7b..5685ce982ace 100644 --- a/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java +++ b/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java @@ -17,6 +17,7 @@ package com.android.server.media; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -26,6 +27,7 @@ import android.media.IMediaRoute2ProviderClient; import android.media.MediaRoute2Info; import android.media.MediaRoute2ProviderInfo; import android.media.MediaRoute2ProviderService; +import android.media.RouteSessionInfo; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -74,6 +76,16 @@ final class MediaRoute2ProviderProxy extends MediaRoute2Provider implements Serv } @Override + public void requestCreateSession(String packageName, String routeId, String controlCategory, + int requestId) { + if (mConnectionReady) { + mActiveConnection.requestCreateSession(packageName, routeId, controlCategory, + requestId); + updateBinding(); + } + } + + @Override public void requestSelectRoute(String packageName, String routeId, int seq) { if (mConnectionReady) { mActiveConnection.requestSelectRoute(packageName, routeId, seq); @@ -268,6 +280,14 @@ final class MediaRoute2ProviderProxy extends MediaRoute2Provider implements Serv mCallback.onRouteSelected(this, packageName, route, controlHints, seq); } + private void onSessionCreated(Connection connection, @Nullable RouteSessionInfo sessionInfo, + @Nullable Bundle controlHints, int requestId) { + if (mActiveConnection != connection) { + return; + } + mCallback.onSessionCreated(this, sessionInfo, controlHints, requestId); + } + private void disconnect() { if (mActiveConnection != null) { mConnectionReady = false; @@ -308,6 +328,15 @@ final class MediaRoute2ProviderProxy extends MediaRoute2Provider implements Serv mClient.dispose(); } + public void requestCreateSession(String packageName, String routeId, String controlCategory, + int requestId) { + try { + mProvider.requestCreateSession(packageName, routeId, controlCategory, requestId); + } catch (RemoteException ex) { + Slog.e(TAG, "Failed to deliver request to create a session.", ex); + } + } + public void requestSelectRoute(String packageName, String routeId, int seq) { try { mProvider.requestSelectRoute(packageName, routeId, seq); @@ -361,6 +390,12 @@ final class MediaRoute2ProviderProxy extends MediaRoute2Provider implements Serv mHandler.post(() -> onRouteSelected(Connection.this, packageName, routeId, controlHints, seq)); } + + void postSessionCreated(@Nullable RouteSessionInfo sessionInfo, + @Nullable Bundle controlHints, int requestId) { + mHandler.post(() -> onSessionCreated(Connection.this, sessionInfo, controlHints, + requestId)); + } } private static final class ProviderClient extends IMediaRoute2ProviderClient.Stub { @@ -391,5 +426,13 @@ final class MediaRoute2ProviderProxy extends MediaRoute2Provider implements Serv } } + @Override + public void notifySessionCreated(@Nullable RouteSessionInfo sessionInfo, + @Nullable Bundle controlHints, int requestId) { + Connection connection = mConnectionRef.get(); + if (connection != null) { + connection.postSessionCreated(sessionInfo, controlHints, requestId); + } + } } } diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index e7b88604db32..8ee5f66e5358 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -29,6 +29,7 @@ import android.media.IMediaRouter2Manager; import android.media.MediaRoute2Info; import android.media.MediaRoute2ProviderInfo; import android.media.MediaRouter2; +import android.media.RouteSessionInfo; import android.os.Binder; import android.os.Bundle; import android.os.Handler; @@ -55,6 +56,7 @@ import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; /** * TODO: Merge this to MediaRouterService once it's finished. @@ -166,44 +168,50 @@ class MediaRouter2ServiceImpl { } } - public void sendControlRequest(@NonNull IMediaRouter2Client client, - @NonNull MediaRoute2Info route, @NonNull Intent request) { + public void requestCreateSession(IMediaRouter2Client client, MediaRoute2Info route, + String controlCategory, int requestId) { Objects.requireNonNull(client, "client must not be null"); Objects.requireNonNull(route, "route must not be null"); - Objects.requireNonNull(request, "request must not be null"); + if (TextUtils.isEmpty(controlCategory)) { + throw new IllegalArgumentException("controlCategory must not be empty"); + } final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { - sendControlRequestLocked(client, route, request); + requestCreateSessionLocked(client, route, controlCategory, requestId); } } finally { Binder.restoreCallingIdentity(token); } } - public void setControlCategories(@NonNull IMediaRouter2Client client, - @NonNull List<String> categories) { + public void sendControlRequest(@NonNull IMediaRouter2Client client, + @NonNull MediaRoute2Info route, @NonNull Intent request) { Objects.requireNonNull(client, "client must not be null"); - Objects.requireNonNull(categories, "categories must not be null"); + Objects.requireNonNull(route, "route must not be null"); + Objects.requireNonNull(request, "request must not be null"); final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { - Client2Record clientRecord = mAllClientRecords.get(client.asBinder()); - setControlCategoriesLocked(clientRecord, categories); + sendControlRequestLocked(client, route, request); } } finally { Binder.restoreCallingIdentity(token); } } - public void requestSelectRoute2(@NonNull IMediaRouter2Client client, - @Nullable MediaRoute2Info route) { + public void setControlCategories(@NonNull IMediaRouter2Client client, + @NonNull List<String> categories) { + Objects.requireNonNull(client, "client must not be null"); + Objects.requireNonNull(categories, "categories must not be null"); + final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { - requestSelectRoute2Locked(mAllClientRecords.get(client.asBinder()), false, route); + Client2Record clientRecord = mAllClientRecords.get(client.asBinder()); + setControlCategoriesLocked(clientRecord, categories); } } finally { Binder.restoreCallingIdentity(token); @@ -353,6 +361,19 @@ class MediaRouter2ServiceImpl { } } + private void requestCreateSessionLocked(@NonNull IMediaRouter2Client client, + @NonNull MediaRoute2Info route, @NonNull String controlCategory, int requestId) { + final IBinder binder = client.asBinder(); + final Client2Record clientRecord = mAllClientRecords.get(binder); + + if (clientRecord != null) { + clientRecord.mUserRecord.mHandler.sendMessage( + obtainMessage(UserHandler::requestCreateSessionOnHandler, + clientRecord.mUserRecord.mHandler, + clientRecord, route, controlCategory, requestId)); + } + } + private void requestSelectRoute2Locked(Client2Record clientRecord, boolean selectedByManager, MediaRoute2Info route) { if (clientRecord != null) { @@ -569,6 +590,7 @@ class MediaRouter2ServiceImpl { mHandler = new UserHandler(MediaRouter2ServiceImpl.this, this); } + // TODO: This assumes that only one client exists in a package. Is it true? Client2Record findClientRecordLocked(String packageName) { for (Client2Record clientRecord : mClientRecords) { if (TextUtils.equals(clientRecord.mPackageName, packageName)) { @@ -667,10 +689,12 @@ class MediaRouter2ServiceImpl { private final SystemMediaRoute2Provider mSystemProvider; private final ArrayList<MediaRoute2Provider> mMediaProviders = new ArrayList<>(); - private final List<MediaRoute2ProviderInfo> mProviderInfos = new ArrayList<>(); + + private final List<MediaRoute2ProviderInfo> mLastProviderInfos = new ArrayList<>(); + private final CopyOnWriteArrayList<SessionCreationRequest> mSessionCreationRequests = + new CopyOnWriteArrayList<>(); private boolean mRunning; - private boolean mProviderInfosUpdateScheduled; UserHandler(MediaRouter2ServiceImpl service, UserRecord userRecord) { super(Looper.getMainLooper(), null, true); @@ -721,23 +745,31 @@ class MediaRouter2ServiceImpl { controlHints, seq)); } + @Override + public void onSessionCreated(@NonNull MediaRoute2Provider provider, + @Nullable RouteSessionInfo sessionInfo, @Nullable Bundle controlHints, + int requestId) { + sendMessage(PooledLambda.obtainMessage(UserHandler::handleCreateSessionResultOnHandler, + this, provider, sessionInfo, controlHints, requestId)); + } + private void updateProvider(MediaRoute2Provider provider) { int providerIndex = getProviderInfoIndex(provider.getUniqueId()); MediaRoute2ProviderInfo providerInfo = provider.getProviderInfo(); MediaRoute2ProviderInfo prevInfo = - (providerIndex < 0) ? null : mProviderInfos.get(providerIndex); + (providerIndex < 0) ? null : mLastProviderInfos.get(providerIndex); if (Objects.equals(prevInfo, providerInfo)) return; if (prevInfo == null) { - mProviderInfos.add(providerInfo); + mLastProviderInfos.add(providerInfo); Collection<MediaRoute2Info> addedRoutes = providerInfo.getRoutes(); if (addedRoutes.size() > 0) { sendMessage(PooledLambda.obtainMessage(UserHandler::notifyRoutesAddedToClients, this, getClients(), new ArrayList<>(addedRoutes))); } } else if (providerInfo == null) { - mProviderInfos.remove(prevInfo); + mLastProviderInfos.remove(prevInfo); Collection<MediaRoute2Info> removedRoutes = prevInfo.getRoutes(); if (removedRoutes.size() > 0) { sendMessage(PooledLambda.obtainMessage( @@ -745,7 +777,7 @@ class MediaRouter2ServiceImpl { this, getClients(), new ArrayList<>(removedRoutes))); } } else { - mProviderInfos.set(providerIndex, providerInfo); + mLastProviderInfos.set(providerIndex, providerInfo); List<MediaRoute2Info> addedRoutes = new ArrayList<>(); List<MediaRoute2Info> removedRoutes = new ArrayList<>(); List<MediaRoute2Info> changedRoutes = new ArrayList<>(); @@ -794,8 +826,8 @@ class MediaRouter2ServiceImpl { } private int getProviderInfoIndex(String providerId) { - for (int i = 0; i < mProviderInfos.size(); i++) { - MediaRoute2ProviderInfo providerInfo = mProviderInfos.get(i); + for (int i = 0; i < mLastProviderInfos.size(); i++) { + MediaRoute2ProviderInfo providerInfo = mLastProviderInfos.get(i); if (TextUtils.equals(providerInfo.getUniqueId(), providerId)) { return i; } @@ -803,6 +835,96 @@ class MediaRouter2ServiceImpl { return -1; } + private void requestCreateSessionOnHandler(Client2Record clientRecord, + MediaRoute2Info route, String controlCategory, int requestId) { + + final MediaRoute2Provider provider = findProvider(route.getProviderId()); + if (provider == null) { + Slog.w(TAG, "Ignoring session creation request since no provider found for" + + " given route=" + route); + notifySessionCreationFailed(clientRecord, requestId); + return; + } + + if (!route.getSupportedCategories().contains(controlCategory)) { + Slog.w(TAG, "Ignoring session creation request since the given route=" + route + + " doesn't support the given category=" + controlCategory); + notifySessionCreationFailed(clientRecord, requestId); + return; + } + + // TODO: Apply timeout for each request (How many seconds should we wait?) + SessionCreationRequest request = new SessionCreationRequest( + clientRecord, route, controlCategory, requestId); + mSessionCreationRequests.add(request); + + provider.requestCreateSession(clientRecord.mPackageName, route.getId(), + controlCategory, requestId); + } + + private void handleCreateSessionResultOnHandler( + @NonNull MediaRoute2Provider provider, @Nullable RouteSessionInfo sessionInfo, + @Nullable Bundle controlHints, int requestId) { + SessionCreationRequest matchingRequest = null; + for (SessionCreationRequest request : mSessionCreationRequests) { + if (request.mRequestId == requestId + && TextUtils.equals( + request.mRoute.getProviderId(), provider.getUniqueId())) { + matchingRequest = request; + break; + } + } + + if (matchingRequest == null) { + Slog.w(TAG, "Ignoring session creation result for unknown request. " + + "requestId=" + requestId + ", sessionInfo=" + sessionInfo); + return; + } + + if (sessionInfo == null) { + // Failed + notifySessionCreationFailed(matchingRequest.mClientRecord, requestId); + return; + } + + String originalRouteId = matchingRequest.mRoute.getId(); + String originalCategory = matchingRequest.mControlCategory; + if (!sessionInfo.getSelectedRoutes().contains(originalRouteId) + || !TextUtils.equals(originalCategory, sessionInfo.getControlCategory())) { + Slog.w(TAG, "Created session doesn't match the original request." + + " originalRouteId=" + originalRouteId + + ", originalCategory=" + originalCategory + + ", requestId=" + requestId + ", sessionInfo=" + sessionInfo); + notifySessionCreationFailed(matchingRequest.mClientRecord, requestId); + return; + } + + // Succeeded + notifySessionCreated(matchingRequest.mClientRecord, sessionInfo, controlHints, + requestId); + // TODO: Tell managers for the session creation + } + + private void notifySessionCreated(Client2Record clientRecord, RouteSessionInfo sessionInfo, + Bundle controlHints, int requestId) { + try { + clientRecord.mClient.notifySessionCreated(sessionInfo, controlHints, requestId); + } catch (RemoteException ex) { + Slog.w(TAG, "Failed to notify client of the session creation." + + " Client probably died.", ex); + } + } + + private void notifySessionCreationFailed(Client2Record clientRecord, int requestId) { + try { + clientRecord.mClient.notifySessionCreated( + null /* sessionInfo */, null /* controlHints */, requestId); + } catch (RemoteException ex) { + Slog.w(TAG, "Failed to notify client of the session creation failure." + + " Client probably died.", ex); + } + } + private void updateSelectedRoute(MediaRoute2ProviderProxy provider, String clientPackageName, MediaRoute2Info selectedRoute, Bundle controlHints, int seq) { @@ -858,6 +980,12 @@ class MediaRouter2ServiceImpl { clientRecord = mUserRecord.findClientRecordLocked(clientPackageName); } + if (clientRecord == null) { + Log.w(TAG, "The client has gone. packageName=" + clientPackageName + + " selectingRoute=" + selectingRoute); + return; + } + if (clientRecord.mSelectingRoute == null || !TextUtils.equals( clientRecord.mSelectingRoute.getUniqueId(), selectingRoute.getUniqueId())) { Log.w(TAG, "Ignoring invalid selectFallbackRoute call. " @@ -951,7 +1079,7 @@ class MediaRouter2ServiceImpl { private void notifyRoutesToClient(IMediaRouter2Client client) { List<MediaRoute2Info> routes = new ArrayList<>(); - for (MediaRoute2ProviderInfo providerInfo : mProviderInfos) { + for (MediaRoute2ProviderInfo providerInfo : mLastProviderInfos) { routes.addAll(providerInfo.getRoutes()); } if (routes.size() == 0) { @@ -964,13 +1092,9 @@ class MediaRouter2ServiceImpl { } } + // TODO: Remove notifyRouteSelected* methods private void notifyRouteSelectedToClient(IMediaRouter2Client client, MediaRoute2Info route, int reason, Bundle controlHints) { - try { - client.notifyRouteSelected(route, reason, controlHints); - } catch (RemoteException ex) { - Slog.w(TAG, "Failed to notify routes selected. Client probably died.", ex); - } } private void notifyRoutesAddedToClients(List<IMediaRouter2Client> clients, @@ -1008,7 +1132,7 @@ class MediaRouter2ServiceImpl { private void notifyRoutesToManager(IMediaRouter2Manager manager) { List<MediaRoute2Info> routes = new ArrayList<>(); - for (MediaRoute2ProviderInfo providerInfo : mProviderInfos) { + for (MediaRoute2ProviderInfo providerInfo : mLastProviderInfos) { routes.addAll(providerInfo.getRoutes()); } if (routes.size() == 0) { @@ -1085,5 +1209,21 @@ class MediaRouter2ServiceImpl { } return null; } + + final class SessionCreationRequest { + public final Client2Record mClientRecord; + public final MediaRoute2Info mRoute; + public final String mControlCategory; + public final int mRequestId; + + SessionCreationRequest(@NonNull Client2Record clientRecord, + @NonNull MediaRoute2Info route, + @NonNull String controlCategory, int requestId) { + mClientRecord = clientRecord; + mRoute = route; + mControlCategory = controlCategory; + mRequestId = requestId; + } + } } } diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index 9c99e8f43639..a280f9171725 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -457,8 +457,9 @@ public final class MediaRouterService extends IMediaRouterService.Stub // Binder call @Override - public void requestSelectRoute2(IMediaRouter2Client client, MediaRoute2Info route) { - mService2.requestSelectRoute2(client, route); + public void requestCreateSession(IMediaRouter2Client client, MediaRoute2Info route, + String controlCategory, int requestId) { + mService2.requestCreateSession(client, route, controlCategory, requestId); } // Binder call diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java index 4f64177ad135..6c4c8d50b41e 100644 --- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java +++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java @@ -88,6 +88,12 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { initializeRoutes(); } + @Override + public void requestCreateSession(String packageName, String routeId, String controlCategory, + int requestId) { + // Do nothing + } + //TODO: implement method @Override public void requestSelectRoute(@NonNull String packageName, @NonNull String routeId, int seq) { |