Merge "MediaRouter2: Add OnCreateSessionListener"
diff --git a/media/java/android/media/IMediaRoute2Provider.aidl b/media/java/android/media/IMediaRoute2Provider.aidl
index aa38e51..a25aff6 100644
--- a/media/java/android/media/IMediaRoute2Provider.aidl
+++ b/media/java/android/media/IMediaRoute2Provider.aidl
@@ -18,13 +18,15 @@
 
 import android.content.Intent;
 import android.media.IMediaRoute2ProviderClient;
+import android.os.Bundle;
 
 /**
  * {@hide}
  */
 oneway interface IMediaRoute2Provider {
     void setClient(IMediaRoute2ProviderClient client);
-    void requestCreateSession(String packageName, String routeId, long requestId);
+    void requestCreateSession(String packageName, String routeId, long requestId,
+            in @nullable Bundle sessionHints);
     void releaseSession(String sessionId);
 
     void selectRoute(String sessionId, String routeId);
diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl
index 2d3e185..5275399 100644
--- a/media/java/android/media/IMediaRouterService.aidl
+++ b/media/java/android/media/IMediaRouterService.aidl
@@ -24,6 +24,7 @@
 import android.media.MediaRouterClientState;
 import android.media.RouteDiscoveryPreference;
 import android.media.RoutingSessionInfo;
+import android.os.Bundle;
 
 /**
  * {@hide}
@@ -52,7 +53,8 @@
     void requestSetVolume2(IMediaRouter2Client client, in MediaRoute2Info route, int volume);
     void requestUpdateVolume2(IMediaRouter2Client client, in MediaRoute2Info route, int direction);
 
-    void requestCreateSession(IMediaRouter2Client client, in MediaRoute2Info route, int requestId);
+    void requestCreateSession(IMediaRouter2Client client, in MediaRoute2Info route, int requestId,
+            in @nullable Bundle sessionHints);
     void setDiscoveryRequest2(IMediaRouter2Client client, in RouteDiscoveryPreference preference);
     void selectRoute(IMediaRouter2Client client, String sessionId, in MediaRoute2Info route);
     void deselectRoute(IMediaRouter2Client client, String sessionId, in MediaRoute2Info route);
diff --git a/media/java/android/media/MediaRoute2ProviderService.java b/media/java/android/media/MediaRoute2ProviderService.java
index 5a3de6d..20ca997 100644
--- a/media/java/android/media/MediaRoute2ProviderService.java
+++ b/media/java/android/media/MediaRoute2ProviderService.java
@@ -161,8 +161,8 @@
      * @param sessionInfo information of the new session.
      *                    The {@link RoutingSessionInfo#getId() id} of the session must be unique.
      * @param requestId id of the previous request to create this session provided in
-     *                  {@link #onCreateSession(String, String, long)}
-     * @see #onCreateSession(String, String, long)
+     *                  {@link #onCreateSession(String, String, long, Bundle)}
+     * @see #onCreateSession(String, String, long, Bundle)
      * @hide
      */
     public final void notifySessionCreated(@NonNull RoutingSessionInfo sessionInfo,
@@ -196,8 +196,8 @@
      * Notifies clients of that the session could not be created.
      *
      * @param requestId id of the previous request to create the session provided in
-     *                  {@link #onCreateSession(String, String, long)}.
-     * @see #onCreateSession(String, String, long)
+     *                  {@link #onCreateSession(String, String, long, Bundle)}.
+     * @see #onCreateSession(String, String, long, Bundle)
      * @hide
      */
     public final void notifySessionCreationFailed(long requestId) {
@@ -290,6 +290,9 @@
      * @param packageName the package name of the application that selected the route
      * @param routeId the id of the route initially being connected
      * @param requestId the id of this session creation request
+     * @param sessionHints an optional bundle of app-specific arguments sent by
+     *                     {@link MediaRouter2}, or null if none. The contents of this bundle
+     *                     may affect the result of session creation.
      *
      * @see RoutingSessionInfo.Builder#Builder(String, String)
      * @see RoutingSessionInfo.Builder#addSelectedRoute(String)
@@ -297,7 +300,7 @@
      * @hide
      */
     public abstract void onCreateSession(@NonNull String packageName, @NonNull String routeId,
-            long requestId);
+            long requestId, @Nullable Bundle sessionHints);
 
     /**
      * Called when the session should be released. A client of the session or system can request
@@ -432,12 +435,14 @@
         }
 
         @Override
-        public void requestCreateSession(String packageName, String routeId, long requestId) {
+        public void requestCreateSession(String packageName, String routeId, long requestId,
+                @Nullable Bundle requestCreateSession) {
             if (!checkCallerisSystem()) {
                 return;
             }
             mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onCreateSession,
-                    MediaRoute2ProviderService.this, packageName, routeId, requestId));
+                    MediaRoute2ProviderService.this, packageName, routeId, requestId,
+                    requestCreateSession));
         }
 
         @Override
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index 971b08d..51d08ec 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -92,6 +92,7 @@
     @GuardedBy("sRouterLock")
     private boolean mShouldUpdateRoutes;
     private volatile List<MediaRoute2Info> mFilteredRoutes = Collections.emptyList();
+    private volatile OnCreateSessionListener mOnCreateSessionListener;
 
     /**
      * Gets an instance of the media router associated with the context.
@@ -281,6 +282,19 @@
     }
 
     /**
+     * Sets an {@link OnCreateSessionListener} to send hints when creating a session.
+     * To send the hints, listener should be set <em>BEFORE</em> calling
+     * {@link #requestCreateSession(MediaRoute2Info)}.
+     *
+     * @param listener A listener to send optional app-specific hints when creating a session.
+     *                 {@code null} for unset.
+     * @hide
+     */
+    public void setOnCreateSessionListener(@Nullable OnCreateSessionListener listener) {
+        mOnCreateSessionListener = listener;
+    }
+
+    /**
      * Requests the media route provider service to create a session with the given route.
      *
      * @param route the route you want to create a session with.
@@ -300,13 +314,24 @@
         SessionCreationRequest request = new SessionCreationRequest(requestId, route);
         mSessionCreationRequests.add(request);
 
+
+        OnCreateSessionListener listener = mOnCreateSessionListener;
+        Bundle sessionHints = null;
+        if (listener != null) {
+            sessionHints = listener.onCreateSession(route);
+            if (sessionHints != null) {
+                sessionHints = new Bundle(sessionHints);
+            }
+        }
+
         Client2 client;
         synchronized (sRouterLock) {
             client = mClient;
         }
         if (client != null) {
             try {
-                mMediaRouterService.requestCreateSession(client, route, requestId);
+                mMediaRouterService.requestCreateSession(client, route, requestId,
+                        sessionHints);
             } catch (RemoteException ex) {
                 Log.e(TAG, "Unable to request to create session.", ex);
                 mHandler.sendMessage(obtainMessage(MediaRouter2::createControllerOnHandler,
@@ -707,6 +732,34 @@
     }
 
     /**
+     * A listener interface to send an optional app-specific hints when creating a session.
+     *
+     * @hide
+     */
+    public interface OnCreateSessionListener {
+        /**
+         * Called when the {@link MediaRouter2} is about to request
+         * the media route provider service to create a session with the given route.
+         * The {@link Bundle} returned here will be sent to media route provider service as a hint
+         * for creating a session.
+         * <p>
+         * To send hints when creating the session, set this listener before calling
+         * {@link #requestCreateSession(MediaRoute2Info)}.
+         * <p>
+         * This will be called on the same thread which calls
+         * {@link #requestCreateSession(MediaRoute2Info)}.
+         *
+         * @param route The route to create session with
+         * @return An optional bundle of app-specific arguments to send to the provider,
+         *         or null if none. The contents of this bundle may affect the result of
+         *         session creation.
+         * @see MediaRoute2ProviderService#onCreateSession(String, String, long, Bundle)
+         */
+        @Nullable
+        Bundle onCreateSession(@NonNull MediaRoute2Info route);
+    }
+
+    /**
      * A class to control media routing session in media route provider.
      * For example, selecting/deselcting/transferring routes to session can be done through this
      * class. Instances are created by {@link MediaRouter2}.
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 cc2d1b1..825b547 100644
--- a/media/tests/MediaRouteProvider/src/com/android/mediarouteprovider/example/SampleMediaRoute2ProviderService.java
+++ b/media/tests/MediaRouteProvider/src/com/android/mediarouteprovider/example/SampleMediaRoute2ProviderService.java
@@ -19,10 +19,12 @@
 import static android.media.MediaRoute2Info.DEVICE_TYPE_REMOTE_SPEAKER;
 import static android.media.MediaRoute2Info.DEVICE_TYPE_REMOTE_TV;
 
+import android.annotation.Nullable;
 import android.content.Intent;
 import android.media.MediaRoute2Info;
 import android.media.MediaRoute2ProviderService;
 import android.media.RoutingSessionInfo;
+import android.os.Bundle;
 import android.os.IBinder;
 import android.text.TextUtils;
 
@@ -167,7 +169,8 @@
     }
 
     @Override
-    public void onCreateSession(String packageName, String routeId, long requestId) {
+    public void onCreateSession(String packageName, String routeId, long requestId,
+            @Nullable Bundle sessionHints) {
         MediaRoute2Info route = mRoutes.get(routeId);
         if (route == null || TextUtils.equals(ROUTE_ID3_SESSION_CREATION_FAILED, routeId)) {
             // Tell the router that session cannot be created by passing null as sessionInfo.
@@ -188,6 +191,8 @@
                 .addSelectedRoute(routeId)
                 .addSelectableRoute(ROUTE_ID4_TO_SELECT_AND_DESELECT)
                 .addTransferrableRoute(ROUTE_ID5_TO_TRANSFER_TO)
+                // Set control hints with given sessionHints
+                .setControlHints(sessionHints)
                 .build();
         notifySessionCreated(sessionInfo, requestId);
         publishRoutes();
diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2Test.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2Test.java
index e782aae..32d03db 100644
--- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2Test.java
+++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2Test.java
@@ -43,12 +43,14 @@
 import android.content.Context;
 import android.media.MediaRoute2Info;
 import android.media.MediaRouter2;
+import android.media.MediaRouter2.OnCreateSessionListener;
 import android.media.MediaRouter2.RouteCallback;
 import android.media.MediaRouter2.RoutingController;
 import android.media.MediaRouter2.SessionCallback;
 import android.media.RouteDiscoveryPreference;
 import android.media.RoutingSessionInfo;
 import android.net.Uri;
+import android.os.Bundle;
 import android.os.Parcel;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.SmallTest;
@@ -80,6 +82,9 @@
 
     private static final int TIMEOUT_MS = 5000;
 
+    private static final String TEST_KEY = "test_key";
+    private static final String TEST_VALUE = "test_value";
+
     @Before
     public void setUp() throws Exception {
         mContext = InstrumentationRegistry.getTargetContext();
@@ -328,6 +333,74 @@
     }
 
     @Test
+    public void testSetOnCreateSessionListener() throws Exception {
+        final List<String> sampleRouteFeature = new ArrayList<>();
+        sampleRouteFeature.add(FEATURE_SAMPLE);
+
+        Map<String, MediaRoute2Info> routes = waitAndGetRoutes(sampleRouteFeature);
+        MediaRoute2Info route = routes.get(ROUTE_ID1);
+        assertNotNull(route);
+
+        final Bundle createSessionHints = new Bundle();
+        createSessionHints.putString(TEST_KEY, TEST_VALUE);
+        final OnCreateSessionListener listener = new OnCreateSessionListener() {
+            @Override
+            public Bundle onCreateSession(MediaRoute2Info route) {
+                return createSessionHints;
+            }
+        };
+
+        final CountDownLatch successLatch = new CountDownLatch(1);
+        final CountDownLatch failureLatch = new CountDownLatch(1);
+        final List<RoutingController> controllers = new ArrayList<>();
+
+        // Create session with this route
+        SessionCallback sessionCallback = new SessionCallback() {
+            @Override
+            public void onSessionCreated(RoutingController controller) {
+                assertNotNull(controller);
+                assertTrue(createRouteMap(controller.getSelectedRoutes()).containsKey(ROUTE_ID1));
+
+                // The SampleMediaRoute2ProviderService supposed to set control hints
+                // with the given creationSessionHints.
+                Bundle controlHints = controller.getControlHints();
+                assertNotNull(controlHints);
+                assertTrue(controlHints.containsKey(TEST_KEY));
+                assertEquals(TEST_VALUE, controlHints.getString(TEST_KEY));
+
+                controllers.add(controller);
+                successLatch.countDown();
+            }
+
+            @Override
+            public void onSessionCreationFailed(MediaRoute2Info requestedRoute) {
+                failureLatch.countDown();
+            }
+        };
+
+        // TODO: Remove this once the MediaRouter2 becomes always connected to the service.
+        RouteCallback routeCallback = new RouteCallback();
+        mRouter2.registerRouteCallback(mExecutor, routeCallback, RouteDiscoveryPreference.EMPTY);
+
+        try {
+            mRouter2.registerSessionCallback(mExecutor, sessionCallback);
+
+            // The SampleMediaRoute2ProviderService supposed to set control hints
+            // with the given creationSessionHints.
+            mRouter2.setOnCreateSessionListener(listener);
+            mRouter2.requestCreateSession(route);
+            assertTrue(successLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+            // onSessionCreationFailed should not be called.
+            assertFalse(failureLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        } finally {
+            releaseControllers(controllers);
+            mRouter2.unregisterRouteCallback(routeCallback);
+            mRouter2.unregisterSessionCallback(sessionCallback);
+        }
+    }
+
+    @Test
     public void testSessionCallbackIsNotCalledAfterUnregistered() throws Exception {
         final List<String> sampleRouteType = new ArrayList<>();
         sampleRouteType.add(FEATURE_SAMPLE);
diff --git a/services/core/java/com/android/server/media/MediaRoute2Provider.java b/services/core/java/com/android/server/media/MediaRoute2Provider.java
index b186771..1cd8aad 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.Intent;
 import android.media.MediaRoute2ProviderInfo;
 import android.media.RoutingSessionInfo;
+import android.os.Bundle;
 
 import com.android.internal.annotations.GuardedBy;
 
@@ -49,7 +50,8 @@
         mCallback = callback;
     }
 
-    public abstract void requestCreateSession(String packageName, String routeId, long requestId);
+    public abstract void requestCreateSession(String packageName, String routeId, long requestId,
+            @Nullable Bundle sessionHints);
     public abstract void releaseSession(String sessionId);
 
     public abstract void selectRoute(String sessionId, String routeId);
diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java
index 4b992be..9761461 100644
--- a/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java
+++ b/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java
@@ -26,6 +26,7 @@
 import android.media.MediaRoute2ProviderInfo;
 import android.media.MediaRoute2ProviderService;
 import android.media.RoutingSessionInfo;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.IBinder.DeathRecipient;
@@ -73,9 +74,11 @@
     }
 
     @Override
-    public void requestCreateSession(String packageName, String routeId, long requestId) {
+    public void requestCreateSession(String packageName, String routeId, long requestId,
+            Bundle sessionHints) {
         if (mConnectionReady) {
-            mActiveConnection.requestCreateSession(packageName, routeId, requestId);
+            mActiveConnection.requestCreateSession(
+                    packageName, routeId, requestId, sessionHints);
             updateBinding();
         }
     }
@@ -427,9 +430,10 @@
             mClient.dispose();
         }
 
-        public void requestCreateSession(String packageName, String routeId, long requestId) {
+        public void requestCreateSession(String packageName, String routeId, long requestId,
+                Bundle sessionHints) {
             try {
-                mProvider.requestCreateSession(packageName, routeId, requestId);
+                mProvider.requestCreateSession(packageName, routeId, requestId, sessionHints);
             } catch (RemoteException ex) {
                 Slog.e(TAG, "Failed to deliver request to create a session.", ex);
             }
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index 45d50b3..fd8b4f5 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -22,6 +22,7 @@
 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.content.Context;
 import android.content.Intent;
@@ -178,14 +179,14 @@
     }
 
     public void requestCreateSession(IMediaRouter2Client client, MediaRoute2Info route,
-            int requestId) {
+            int requestId, Bundle sessionHints) {
         Objects.requireNonNull(client, "client must not be null");
         Objects.requireNonNull(route, "route must not be null");
 
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mLock) {
-                requestCreateSessionLocked(client, route, requestId);
+                requestCreateSessionLocked(client, route, requestId, sessionHints);
             }
         } finally {
             Binder.restoreCallingIdentity(token);
@@ -446,7 +447,7 @@
     }
 
     private void requestCreateSessionLocked(@NonNull IMediaRouter2Client client,
-            @NonNull MediaRoute2Info route, long requestId) {
+            @NonNull MediaRoute2Info route, long requestId, @Nullable Bundle sessionHints) {
         final IBinder binder = client.asBinder();
         final Client2Record clientRecord = mAllClientRecords.get(binder);
 
@@ -458,7 +459,8 @@
         if (clientRecord != null) {
             clientRecord.mUserRecord.mHandler.sendMessage(
                     obtainMessage(UserHandler::requestCreateSessionOnHandler,
-                            clientRecord.mUserRecord.mHandler, clientRecord, route, requestId));
+                            clientRecord.mUserRecord.mHandler,
+                            clientRecord, route, requestId, sessionHints));
         }
     }
 
@@ -617,9 +619,9 @@
             }
             long uniqueRequestId = toUniqueRequestId(managerRecord.mClientId, requestId);
             if (clientRecord != null && managerRecord.mTrusted) {
-                //TODO: select route feature properly
+                //TODO: Use client's OnCreateSessionListener to send proper session hints.
                 requestCreateSessionLocked(clientRecord.mClient, route,
-                        uniqueRequestId);
+                        uniqueRequestId, null /* sessionHints */);
             }
         }
     }
@@ -980,7 +982,7 @@
         }
 
         private void requestCreateSessionOnHandler(Client2Record clientRecord,
-                MediaRoute2Info route, long requestId) {
+                MediaRoute2Info route, long requestId, @Nullable Bundle sessionHints) {
 
             final MediaRoute2Provider provider = findProvider(route.getProviderId());
             if (provider == null) {
@@ -996,7 +998,7 @@
             mSessionCreationRequests.add(request);
 
             provider.requestCreateSession(clientRecord.mPackageName, route.getOriginalId(),
-                    requestId);
+                    requestId, sessionHints);
         }
 
         private void selectRouteOnHandler(@NonNull Client2Record clientRecord,
diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java
index aad9636..40fca39 100644
--- a/services/core/java/com/android/server/media/MediaRouterService.java
+++ b/services/core/java/com/android/server/media/MediaRouterService.java
@@ -42,6 +42,7 @@
 import android.media.RouteDiscoveryPreference;
 import android.media.RoutingSessionInfo;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
@@ -460,8 +461,8 @@
     // Binder call
     @Override
     public void requestCreateSession(IMediaRouter2Client client, MediaRoute2Info route,
-            int requestId) {
-        mService2.requestCreateSession(client, route, requestId);
+            int requestId, Bundle sessionHints) {
+        mService2.requestCreateSession(client, route, requestId, sessionHints);
     }
 
     // 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 3759ba9..56c33fe 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
@@ -26,6 +26,7 @@
 import android.media.IAudioService;
 import android.media.MediaRoute2Info;
 import android.media.MediaRoute2ProviderInfo;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.RemoteException;
@@ -96,7 +97,8 @@
     }
 
     @Override
-    public void requestCreateSession(String packageName, String routeId, long requestId) {
+    public void requestCreateSession(String packageName, String routeId, long requestId,
+            Bundle sessionHints) {
         // Do nothing
     }