MediaRoute2Provider: Revisit session related APIs

Bug: 147583303
Test: atest mediaroutertest
Change-Id: I5fbc9d9265607bb8f48a2ca16708dbb0d4979501
diff --git a/media/java/android/media/IMediaRoute2ProviderClient.aidl b/media/java/android/media/IMediaRoute2ProviderClient.aidl
index e8abfdc..0fccb3a 100644
--- a/media/java/android/media/IMediaRoute2ProviderClient.aidl
+++ b/media/java/android/media/IMediaRoute2ProviderClient.aidl
@@ -25,8 +25,10 @@
  * @hide
  */
 oneway interface IMediaRoute2ProviderClient {
-    void updateState(in MediaRoute2ProviderInfo providerInfo,
-            in List<RoutingSessionInfo> sessionInfos);
-    void notifySessionCreated(in @nullable RoutingSessionInfo sessionInfo, long requestId);
-    void notifySessionInfoChanged(in RoutingSessionInfo sessionInfo);
+    // TODO: Change it to updateRoutes?
+    void updateState(in MediaRoute2ProviderInfo providerInfo);
+    void notifySessionCreated(in RoutingSessionInfo sessionInfo, long requestId);
+    void notifySessionCreationFailed(long requestId);
+    void notifySessionUpdated(in RoutingSessionInfo sessionInfo);
+    void notifySessionReleased(in RoutingSessionInfo sessionInfo);
 }
diff --git a/media/java/android/media/MediaRoute2ProviderService.java b/media/java/android/media/MediaRoute2ProviderService.java
index 1cd5dfa..6d9aea5 100644
--- a/media/java/android/media/MediaRoute2ProviderService.java
+++ b/media/java/android/media/MediaRoute2ProviderService.java
@@ -58,6 +58,16 @@
 
     public static final String SERVICE_INTERFACE = "android.media.MediaRoute2ProviderService";
 
+    /**
+     * The request ID to pass {@link #notifySessionCreated(RoutingSessionInfo, long)}
+     * when {@link MediaRoute2ProviderService} created a session although there was no creation
+     * request.
+     *
+     * @see #notifySessionCreated(RoutingSessionInfo, long)
+     * @hide
+     */
+    public static final long REQUEST_ID_UNKNOWN = 0;
+
     private final Handler mHandler;
     private final Object mSessionLock = new Object();
     private final AtomicBoolean mStatePublishScheduled = new AtomicBoolean(false);
@@ -118,7 +128,7 @@
      *
      * @param sessionId id of the session
      * @return information of the session with the given id.
-     *         null if the session is destroyed or id is not valid.
+     *         null if the session is released or ID is not valid.
      * @hide
      */
     @Nullable
@@ -143,90 +153,39 @@
     }
 
     /**
-     * Updates the information of a session.
-     * If the session is destroyed or not created before, it will be ignored.
-     * Call {@link #updateProviderInfo(MediaRoute2ProviderInfo)} to notify clients of
-     * session info changes.
-     *
-     * @param sessionInfo new session information
-     * @see #notifySessionCreated(RoutingSessionInfo, long)
-     * @hide
-     */
-    public final void updateSessionInfo(@NonNull RoutingSessionInfo sessionInfo) {
-        Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
-        String sessionId = sessionInfo.getId();
-
-        synchronized (mSessionLock) {
-            if (mSessionInfo.containsKey(sessionId)) {
-                mSessionInfo.put(sessionId, sessionInfo);
-                schedulePublishState();
-            } else {
-                Log.w(TAG, "Ignoring unknown session info.");
-                return;
-            }
-        }
-    }
-
-    /**
-     * Notifies the session is changed.
-     *
-     * TODO: This method is temporary, only created for tests. Remove when the alternative is ready.
-     * @hide
-     */
-    public final void notifySessionInfoChanged(@NonNull RoutingSessionInfo sessionInfo) {
-        Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
-
-        String sessionId = sessionInfo.getId();
-        synchronized (mSessionLock) {
-            if (mSessionInfo.containsKey(sessionId)) {
-                mSessionInfo.put(sessionId, sessionInfo);
-            } else {
-                Log.w(TAG, "Ignoring unknown session info.");
-                return;
-            }
-        }
-
-        if (mClient == null) {
-            return;
-        }
-        try {
-            mClient.notifySessionInfoChanged(sessionInfo);
-        } catch (RemoteException ex) {
-            Log.w(TAG, "Failed to notify session info changed.");
-        }
-    }
-
-    /**
-     * 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.
+     * Notifies clients of that the session is created and ready for use.
+     * <p>
+     * If this session is created without any creation request, use {@link #REQUEST_ID_UNKNOWN}
+     * as the request ID.
      *
      * @param sessionInfo information of the new session.
-     *                    The {@link RoutingSessionInfo#getId() id} of the session must be
-     *                    unique. Pass {@code null} to reject the request or inform clients that
-     *                    session creation is failed.
-     * @param requestId id of the previous request to create this 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, String, long)}
+     * @see #onCreateSession(String, String, String, long)
      * @hide
      */
-    // TODO: fail reason?
-    // TODO: Maybe better to create notifySessionCreationFailed?
-    public final void notifySessionCreated(@Nullable RoutingSessionInfo sessionInfo,
+    public final void notifySessionCreated(@NonNull RoutingSessionInfo sessionInfo,
             long requestId) {
-        if (sessionInfo != null) {
-            String sessionId = sessionInfo.getId();
-            synchronized (mSessionLock) {
-                if (mSessionInfo.containsKey(sessionId)) {
-                    Log.w(TAG, "Ignoring duplicate session id.");
-                    return;
-                }
-                mSessionInfo.put(sessionInfo.getId(), sessionInfo);
+        Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
+
+        String sessionId = sessionInfo.getId();
+        synchronized (mSessionLock) {
+            if (mSessionInfo.containsKey(sessionId)) {
+                Log.w(TAG, "Ignoring duplicate session id.");
+                return;
             }
-            schedulePublishState();
+            mSessionInfo.put(sessionInfo.getId(), sessionInfo);
         }
+        schedulePublishState();
 
         if (mClient == null) {
             return;
         }
         try {
+            // TODO: Calling binder calls in multiple thread may cause timing issue.
+            //       Consider to change implementations to avoid the problems.
+            //       For example, post binder calls, always send all sessions at once, etc.
             mClient.notifySessionCreated(sessionInfo, requestId);
         } catch (RemoteException ex) {
             Log.w(TAG, "Failed to notify session created.");
@@ -234,70 +193,138 @@
     }
 
     /**
-     * Releases a session with the given id.
-     * {@link #onDestroySession} is called if the session is released.
+     * Notifies clients of that the session could not be created.
      *
-     * @param sessionId id of the session to be released
-     * @see #onDestroySession(String, RoutingSessionInfo)
+     * @param requestId id of the previous request to create the session provided in
+     *                  {@link #onCreateSession(String, String, String, long)}.
+     * @see #onCreateSession(String, String, String, long)
      * @hide
      */
-    public final void releaseSession(@NonNull String sessionId) {
-        if (TextUtils.isEmpty(sessionId)) {
-            throw new IllegalArgumentException("sessionId must not be empty");
+    public final void notifySessionCreationFailed(long requestId) {
+        if (mClient == null) {
+            return;
         }
-        //TODO: notify media router service of release.
-        RoutingSessionInfo sessionInfo;
-        synchronized (mSessionLock) {
-            sessionInfo = mSessionInfo.remove(sessionId);
-        }
-        if (sessionInfo != null) {
-            mHandler.sendMessage(obtainMessage(
-                    MediaRoute2ProviderService::onDestroySession, this, sessionId, sessionInfo));
-            schedulePublishState();
+        try {
+            mClient.notifySessionCreationFailed(requestId);
+        } catch (RemoteException ex) {
+            Log.w(TAG, "Failed to notify session creation failed.");
         }
     }
 
     /**
-     * Called when a session should be created.
+     * Notifies the existing session is updated. For example, when
+     * {@link RoutingSessionInfo#getSelectedRoutes() selected routes} are changed.
+     *
+     * @hide
+     */
+    public final void notifySessionUpdated(@NonNull RoutingSessionInfo sessionInfo) {
+        Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
+
+        String sessionId = sessionInfo.getId();
+        synchronized (mSessionLock) {
+            if (mSessionInfo.containsKey(sessionId)) {
+                mSessionInfo.put(sessionId, sessionInfo);
+            } else {
+                Log.w(TAG, "Ignoring unknown session info.");
+                return;
+            }
+        }
+
+        if (mClient == null) {
+            return;
+        }
+        try {
+            mClient.notifySessionUpdated(sessionInfo);
+        } catch (RemoteException ex) {
+            Log.w(TAG, "Failed to notify session info changed.");
+        }
+    }
+
+    /**
+     * Notifies that the session is released.
+     *
+     * @param sessionId id of the released session.
+     * @see #onReleaseSession(String)
+     * @hide
+     */
+    public final void notifySessionReleased(@NonNull String sessionId) {
+        if (TextUtils.isEmpty(sessionId)) {
+            throw new IllegalArgumentException("sessionId must not be empty");
+        }
+        RoutingSessionInfo sessionInfo;
+        synchronized (mSessionLock) {
+            sessionInfo = mSessionInfo.remove(sessionId);
+        }
+
+        if (sessionInfo == null) {
+            Log.w(TAG, "Ignoring unknown session info.");
+            return;
+        }
+
+        if (mClient == null) {
+            return;
+        }
+        try {
+            mClient.notifySessionReleased(sessionInfo);
+        } catch (RemoteException ex) {
+            Log.w(TAG, "Failed to notify session info changed.");
+        }
+    }
+
+    /**
+     * Called when the service receives a request to create a session.
+     * <p>
      * You should create and maintain your own session and notifies the client of
      * session info. Call {@link #notifySessionCreated(RoutingSessionInfo, long)}
      * 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(RoutingSessionInfo, long)}
-     * with the given {@code requestId}.
+     * The created session must have the same route feature and must include the given route
+     * specified by {@code routeId}.
+     * <p>
+     * If the session can be controlled, you can optionally pass the control hints to
+     * {@link RoutingSessionInfo.Builder#setControlHints(Bundle)}. Control hints is a
+     * {@link Bundle} which contains how to control the session.
+     * <p>
+     * If you can't create the session or want to reject the request, call
+     * {@link #notifySessionCreationFailed(long)} 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 routeFeature the route feature of the new session
      * @param requestId the id of this session creation request
+     *
+     * @see RoutingSessionInfo.Builder#Builder(String, String, String)
+     * @see RoutingSessionInfo.Builder#addSelectedRoute(String)
+     * @see RoutingSessionInfo.Builder#setControlHints(Bundle)
      * @hide
      */
     public abstract void onCreateSession(@NonNull String packageName, @NonNull String routeId,
             @NonNull String routeFeature, long requestId);
 
     /**
-     * Called when a session is about to be destroyed.
-     * You can clean up your session here. This can happen by the
-     * client or provider itself.
+     * Called when the session should be released. A client of the session or system can request
+     * a session to be released.
+     * <p>
+     * After releasing the session, call {@link #notifySessionReleased(String)}
+     * with the ID of the released session.
      *
-     * @param sessionId id of the session being destroyed.
-     * @param lastSessionInfo information of the session being destroyed.
-     * @see #releaseSession(String)
+     * Note: Calling {@link #notifySessionReleased(String)} will <em>NOT</em> trigger
+     * this method to be called.
+     *
+     * @param sessionId id of the session being released.
+     * @see #notifySessionReleased(String)
+     * @see #getSessionInfo(String)
      * @hide
      */
-    public abstract void onDestroySession(@NonNull String sessionId,
-            @NonNull RoutingSessionInfo lastSessionInfo);
+    public abstract void onReleaseSession(@NonNull String sessionId);
 
     //TODO: make a way to reject the request
     /**
      * Called when a client requests selecting a route for the session.
-     * After the route is selected, call {@link #updateSessionInfo(RoutingSessionInfo)} to update
-     * session info and call {@link #updateProviderInfo(MediaRoute2ProviderInfo)} to notify
-     * clients of updated session info.
+     * After the route is selected, call {@link #notifySessionUpdated(RoutingSessionInfo)}
+     * to update session info.
      *
      * @param sessionId id of the session
      * @param routeId id of the route
-     * @see #updateSessionInfo(RoutingSessionInfo)
      * @hide
      */
     public abstract void onSelectRoute(@NonNull String sessionId, @NonNull String routeId);
@@ -305,9 +332,8 @@
     //TODO: make a way to reject the request
     /**
      * Called when a client requests deselecting a route from the session.
-     * After the route is deselected, call {@link #updateSessionInfo(RoutingSessionInfo)} to update
-     * session info and call {@link #updateProviderInfo(MediaRoute2ProviderInfo)} to notify
-     * clients of updated session info.
+     * After the route is deselected, call {@link #notifySessionUpdated(RoutingSessionInfo)}
+     * to update session info.
      *
      * @param sessionId id of the session
      * @param routeId id of the route
@@ -318,9 +344,8 @@
     //TODO: make a way to reject the request
     /**
      * Called when a client requests transferring a session to a route.
-     * After the transfer is finished, call {@link #updateSessionInfo(RoutingSessionInfo)} to update
-     * session info and call {@link #updateProviderInfo(MediaRoute2ProviderInfo)} to notify
-     * clients of updated session info.
+     * After the transfer is finished, call {@link #notifySessionUpdated(RoutingSessionInfo)}
+     * to update session info.
      *
      * @param sessionId id of the session
      * @param routeId id of the route
@@ -344,6 +369,8 @@
      * </p>
      *
      * @param preference the new discovery preference
+     *
+     * TODO: This method needs tests.
      */
     public void onDiscoveryPreferenceChanged(@NonNull RouteDiscoveryPreference preference) {}
 
@@ -383,7 +410,7 @@
             sessionInfos = new ArrayList<>(mSessionInfo.values());
         }
         try {
-            mClient.updateState(mProviderInfo, sessionInfos);
+            mClient.updateState(mProviderInfo);
         } catch (RemoteException ex) {
             Log.w(TAG, "Failed to send onProviderInfoUpdated");
         }
@@ -415,6 +442,7 @@
                     MediaRoute2ProviderService.this, packageName, routeId, routeFeature,
                     requestId));
         }
+
         @Override
         public void releaseSession(@NonNull String sessionId) {
             if (!checkCallerisSystem()) {
@@ -424,7 +452,7 @@
                 Log.w(TAG, "releaseSession: Ignoring empty sessionId from system service.");
                 return;
             }
-            mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::releaseSession,
+            mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onReleaseSession,
                     MediaRoute2ProviderService.this, sessionId));
         }
 
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 221783b..ed93112 100644
--- a/media/tests/MediaRouteProvider/src/com/android/mediarouteprovider/example/SampleMediaRoute2ProviderService.java
+++ b/media/tests/MediaRouteProvider/src/com/android/mediarouteprovider/example/SampleMediaRoute2ProviderService.java
@@ -172,7 +172,7 @@
         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.
-            notifySessionCreated(/* sessionInfo= */ null, requestId);
+            notifySessionCreationFailed(requestId);
             return;
         }
         maybeDeselectRoute(routeId);
@@ -196,8 +196,13 @@
     }
 
     @Override
-    public void onDestroySession(String sessionId, RoutingSessionInfo lastSessionInfo) {
-        for (String routeId : lastSessionInfo.getSelectedRoutes()) {
+    public void onReleaseSession(String sessionId) {
+        RoutingSessionInfo sessionInfo = getSessionInfo(sessionId);
+        if (sessionInfo == null) {
+            return;
+        }
+
+        for (String routeId : sessionInfo.getSelectedRoutes()) {
             mRouteIdToSessionId.remove(routeId);
             MediaRoute2Info route = mRoutes.get(routeId);
             if (route != null) {
@@ -206,6 +211,7 @@
                         .build());
             }
         }
+        notifySessionReleased(sessionId);
     }
 
     @Override
@@ -227,8 +233,7 @@
                 .removeSelectableRoute(routeId)
                 .addDeselectableRoute(routeId)
                 .build();
-        updateSessionInfo(newSessionInfo);
-        notifySessionInfoChanged(newSessionInfo);
+        notifySessionUpdated(newSessionInfo);
     }
 
     @Override
@@ -247,7 +252,7 @@
                 .build());
 
         if (sessionInfo.getSelectedRoutes().size() == 1) {
-            releaseSession(sessionId);
+            notifySessionReleased(sessionId);
             return;
         }
 
@@ -256,8 +261,7 @@
                 .addSelectableRoute(routeId)
                 .removeDeselectableRoute(routeId)
                 .build();
-        updateSessionInfo(newSessionInfo);
-        notifySessionInfoChanged(newSessionInfo);
+        notifySessionUpdated(newSessionInfo);
     }
 
     @Override
@@ -269,8 +273,7 @@
                 .removeDeselectableRoute(routeId)
                 .removeTransferrableRoute(routeId)
                 .build();
-        updateSessionInfo(newSessionInfo);
-        notifySessionInfoChanged(newSessionInfo);
+        notifySessionUpdated(newSessionInfo);
     }
 
     void maybeDeselectRoute(String routeId) {
diff --git a/services/core/java/com/android/server/media/MediaRoute2Provider.java b/services/core/java/com/android/server/media/MediaRoute2Provider.java
index 0d315cd..408c1c9 100644
--- a/services/core/java/com/android/server/media/MediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/MediaRoute2Provider.java
@@ -23,18 +23,22 @@
 import android.media.MediaRoute2ProviderInfo;
 import android.media.RoutingSessionInfo;
 
+import com.android.internal.annotations.GuardedBy;
+
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 
 abstract class MediaRoute2Provider {
     final ComponentName mComponentName;
     final String mUniqueId;
+    final Object mLock = new Object();
 
     Callback mCallback;
     private volatile MediaRoute2ProviderInfo mProviderInfo;
-    private volatile List<RoutingSessionInfo> mSessionInfos = Collections.emptyList();
+
+    @GuardedBy("mLock")
+    final List<RoutingSessionInfo> mSessionInfos = new ArrayList<>();
 
     MediaRoute2Provider(@NonNull ComponentName componentName) {
         mComponentName = Objects.requireNonNull(componentName, "Component name must not be null.");
@@ -69,11 +73,12 @@
 
     @NonNull
     public List<RoutingSessionInfo> getSessionInfos() {
-        return mSessionInfos;
+        synchronized (mLock) {
+            return mSessionInfos;
+        }
     }
 
-    void setProviderState(MediaRoute2ProviderInfo providerInfo,
-            List<RoutingSessionInfo> sessionInfos) {
+    void setProviderState(MediaRoute2ProviderInfo providerInfo) {
         if (providerInfo == null) {
             mProviderInfo = null;
         } else {
@@ -81,14 +86,6 @@
                     .setUniqueId(mUniqueId)
                     .build();
         }
-        List<RoutingSessionInfo> sessionInfoWithProviderId = new ArrayList<RoutingSessionInfo>();
-        for (RoutingSessionInfo sessionInfo : sessionInfos) {
-            sessionInfoWithProviderId.add(
-                    new RoutingSessionInfo.Builder(sessionInfo)
-                            .setProviderId(mUniqueId)
-                            .build());
-        }
-        mSessionInfos = sessionInfoWithProviderId;
     }
 
     void notifyProviderState() {
@@ -97,9 +94,8 @@
         }
     }
 
-    void setAndNotifyProviderState(MediaRoute2ProviderInfo providerInfo,
-            List<RoutingSessionInfo> sessionInfos) {
-        setProviderState(providerInfo, sessionInfos);
+    void setAndNotifyProviderState(MediaRoute2ProviderInfo providerInfo) {
+        setProviderState(providerInfo);
         notifyProviderState();
     }
 
@@ -112,10 +108,9 @@
         void onProviderStateChanged(@Nullable MediaRoute2Provider provider);
         void onSessionCreated(@NonNull MediaRoute2Provider provider,
                 @Nullable RoutingSessionInfo sessionInfo, long requestId);
-        // TODO: Remove this when MediaRouter2ServiceImpl notifies clients of session changes.
-        void onSessionInfoChanged(@NonNull MediaRoute2Provider provider,
+        void onSessionCreationFailed(@NonNull MediaRoute2Provider provider, long requestId);
+        void onSessionUpdated(@NonNull MediaRoute2Provider provider,
                 @NonNull RoutingSessionInfo sessionInfo);
-        // TODO: Call this when service actually notifies of session release.
         void onSessionReleased(@NonNull MediaRoute2Provider provider,
                 @NonNull RoutingSessionInfo sessionInfo);
     }
diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java
index c0ad46f..3840d02 100644
--- a/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java
+++ b/services/core/java/com/android/server/media/MediaRoute2ProviderProxy.java
@@ -17,7 +17,6 @@
 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;
@@ -37,8 +36,6 @@
 
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
-import java.util.Collections;
-import java.util.List;
 import java.util.Objects;
 
 /**
@@ -270,35 +267,69 @@
     }
 
     private void onProviderStateUpdated(Connection connection,
-            MediaRoute2ProviderInfo providerInfo, List<RoutingSessionInfo> sessionInfos) {
+            MediaRoute2ProviderInfo providerInfo) {
         if (mActiveConnection != connection) {
             return;
         }
         if (DEBUG) {
             Slog.d(TAG, this + ": State changed ");
         }
-        setAndNotifyProviderState(providerInfo, sessionInfos);
+        setAndNotifyProviderState(providerInfo);
     }
 
-    private void onSessionCreated(Connection connection, @Nullable RoutingSessionInfo sessionInfo,
+    private void onSessionCreated(Connection connection, RoutingSessionInfo sessionInfo,
             long requestId) {
         if (mActiveConnection != connection) {
             return;
         }
-        if (sessionInfo != null) {
-            sessionInfo = new RoutingSessionInfo.Builder(sessionInfo)
-                    .setProviderId(getUniqueId())
-                    .build();
+
+        if (sessionInfo == null) {
+            Slog.w(TAG, "onSessionCreated: Ignoring null sessionInfo sent from " + mComponentName);
+            return;
         }
+
+        sessionInfo = new RoutingSessionInfo.Builder(sessionInfo)
+                .setProviderId(getUniqueId())
+                .build();
+
+        boolean duplicateSessionAlreadyExists = false;
+        synchronized (mLock) {
+            for (int i = 0; i < mSessionInfos.size(); i++) {
+                if (mSessionInfos.get(i).getId().equals(sessionInfo.getId())) {
+                    duplicateSessionAlreadyExists = true;
+                    break;
+                }
+            }
+            mSessionInfos.add(sessionInfo);
+        }
+
+        if (duplicateSessionAlreadyExists) {
+            Slog.w(TAG, "onSessionCreated: Duplicate session already exists. Ignoring.");
+            return;
+        }
+
         mCallback.onSessionCreated(this, sessionInfo, requestId);
     }
 
-    private void onSessionInfoChanged(Connection connection, RoutingSessionInfo sessionInfo) {
+    private void onSessionCreationFailed(Connection connection, long requestId) {
+        if (mActiveConnection != connection) {
+            return;
+        }
+
+        if (requestId == MediaRoute2ProviderService.REQUEST_ID_UNKNOWN) {
+            Slog.w(TAG, "onSessionCreationFailed: Ignoring requestId REQUEST_ID_UNKNOWN");
+            return;
+        }
+
+        mCallback.onSessionCreationFailed(this, requestId);
+    }
+
+    private void onSessionUpdated(Connection connection, RoutingSessionInfo sessionInfo) {
         if (mActiveConnection != connection) {
             return;
         }
         if (sessionInfo == null) {
-            Slog.w(TAG, "onSessionInfoChanged: Ignoring null sessionInfo sent from "
+            Slog.w(TAG, "onSessionUpdated: Ignoring null sessionInfo sent from "
                     + mComponentName);
             return;
         }
@@ -307,7 +338,55 @@
                 .setProviderId(getUniqueId())
                 .build();
 
-        mCallback.onSessionInfoChanged(this, sessionInfo);
+        boolean found = false;
+        synchronized (mLock) {
+            for (int i = 0; i < mSessionInfos.size(); i++) {
+                if (mSessionInfos.get(i).getId().equals(sessionInfo.getId())) {
+                    mSessionInfos.set(i, sessionInfo);
+                    found = true;
+                    break;
+                }
+            }
+        }
+
+        if (!found) {
+            Slog.w(TAG, "onSessionUpdated: Matching session info not found");
+            return;
+        }
+
+        mCallback.onSessionUpdated(this, sessionInfo);
+    }
+
+    private void onSessionReleased(Connection connection, RoutingSessionInfo sessionInfo) {
+        if (mActiveConnection != connection) {
+            return;
+        }
+        if (sessionInfo == null) {
+            Slog.w(TAG, "onSessionReleased: Ignoring null sessionInfo sent from " + mComponentName);
+            return;
+        }
+
+        sessionInfo = new RoutingSessionInfo.Builder(sessionInfo)
+                .setProviderId(getUniqueId())
+                .build();
+
+        boolean found = false;
+        synchronized (mLock) {
+            for (int i = 0; i < mSessionInfos.size(); i++) {
+                if (mSessionInfos.get(i).getId().equals(sessionInfo.getId())) {
+                    mSessionInfos.remove(i);
+                    found = true;
+                    break;
+                }
+            }
+        }
+
+        if (!found) {
+            Slog.w(TAG, "onSessionReleased: Matching session info not found");
+            return;
+        }
+
+        mCallback.onSessionReleased(this, sessionInfo);
     }
 
     private void disconnect() {
@@ -315,7 +394,7 @@
             mConnectionReady = false;
             mActiveConnection.dispose();
             mActiveConnection = null;
-            setAndNotifyProviderState(null, Collections.emptyList());
+            setAndNotifyProviderState(null);
         }
     }
 
@@ -421,19 +500,24 @@
             mHandler.post(() -> onConnectionDied(Connection.this));
         }
 
-        void postProviderStateUpdated(MediaRoute2ProviderInfo providerInfo,
-                List<RoutingSessionInfo> sessionInfos) {
-            mHandler.post(() -> onProviderStateUpdated(Connection.this,
-                    providerInfo, sessionInfos));
+        void postProviderStateUpdated(MediaRoute2ProviderInfo providerInfo) {
+            mHandler.post(() -> onProviderStateUpdated(Connection.this, providerInfo));
         }
 
-        void postSessionCreated(@Nullable RoutingSessionInfo sessionInfo, long requestId) {
-            mHandler.post(() -> onSessionCreated(Connection.this, sessionInfo,
-                    requestId));
+        void postSessionCreated(RoutingSessionInfo sessionInfo, long requestId) {
+            mHandler.post(() -> onSessionCreated(Connection.this, sessionInfo, requestId));
         }
 
-        void postSessionInfoChanged(RoutingSessionInfo sessionInfo) {
-            mHandler.post(() -> onSessionInfoChanged(Connection.this, sessionInfo));
+        void postSessionCreationFailed(long requestId) {
+            mHandler.post(() -> onSessionCreationFailed(Connection.this, requestId));
+        }
+
+        void postSessionUpdated(RoutingSessionInfo sessionInfo) {
+            mHandler.post(() -> onSessionUpdated(Connection.this, sessionInfo));
+        }
+
+        void postSessionReleased(RoutingSessionInfo sessionInfo) {
+            mHandler.post(() -> onSessionReleased(Connection.this, sessionInfo));
         }
     }
 
@@ -449,16 +533,15 @@
         }
 
         @Override
-        public void updateState(MediaRoute2ProviderInfo providerInfo,
-                List<RoutingSessionInfo> sessionInfos) {
+        public void updateState(MediaRoute2ProviderInfo providerInfo) {
             Connection connection = mConnectionRef.get();
             if (connection != null) {
-                connection.postProviderStateUpdated(providerInfo, sessionInfos);
+                connection.postProviderStateUpdated(providerInfo);
             }
         }
 
         @Override
-        public void notifySessionCreated(@Nullable RoutingSessionInfo sessionInfo, long requestId) {
+        public void notifySessionCreated(RoutingSessionInfo sessionInfo, long requestId) {
             Connection connection = mConnectionRef.get();
             if (connection != null) {
                 connection.postSessionCreated(sessionInfo, requestId);
@@ -466,10 +549,26 @@
         }
 
         @Override
-        public void notifySessionInfoChanged(RoutingSessionInfo sessionInfo) {
+        public void notifySessionCreationFailed(long requestId) {
             Connection connection = mConnectionRef.get();
             if (connection != null) {
-                connection.postSessionInfoChanged(sessionInfo);
+                connection.postSessionCreationFailed(requestId);
+            }
+        }
+
+        @Override
+        public void notifySessionUpdated(RoutingSessionInfo sessionInfo) {
+            Connection connection = mConnectionRef.get();
+            if (connection != null) {
+                connection.postSessionUpdated(sessionInfo);
+            }
+        }
+
+        @Override
+        public void notifySessionReleased(RoutingSessionInfo sessionInfo) {
+            Connection connection = mConnectionRef.get();
+            if (connection != null) {
+                connection.postSessionReleased(sessionInfo);
             }
         }
     }
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index c48c90d..d940e35 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -22,7 +22,6 @@
 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;
@@ -31,6 +30,7 @@
 import android.media.IMediaRouter2Manager;
 import android.media.MediaRoute2Info;
 import android.media.MediaRoute2ProviderInfo;
+import android.media.MediaRoute2ProviderService;
 import android.media.RouteDiscoveryPreference;
 import android.media.RoutingSessionInfo;
 import android.os.Binder;
@@ -876,13 +876,19 @@
 
         @Override
         public void onSessionCreated(@NonNull MediaRoute2Provider provider,
-                @Nullable RoutingSessionInfo sessionInfo, long requestId) {
+                @NonNull RoutingSessionInfo sessionInfo, long requestId) {
             sendMessage(PooledLambda.obtainMessage(UserHandler::onSessionCreatedOnHandler,
                     this, provider, sessionInfo, requestId));
         }
 
         @Override
-        public void onSessionInfoChanged(@NonNull MediaRoute2Provider provider,
+        public void onSessionCreationFailed(@NonNull MediaRoute2Provider provider, long requestId) {
+            sendMessage(PooledLambda.obtainMessage(UserHandler::onSessionCreationFailedOnHandler,
+                    this, provider, requestId));
+        }
+
+        @Override
+        public void onSessionUpdated(@NonNull MediaRoute2Provider provider,
                 @NonNull RoutingSessionInfo sessionInfo) {
             sendMessage(PooledLambda.obtainMessage(UserHandler::onSessionInfoChangedOnHandler,
                     this, provider, sessionInfo));
@@ -1132,7 +1138,14 @@
         }
 
         private void onSessionCreatedOnHandler(@NonNull MediaRoute2Provider provider,
-                @Nullable RoutingSessionInfo sessionInfo, long requestId) {
+                @NonNull RoutingSessionInfo sessionInfo, long requestId) {
+
+            if (requestId == MediaRoute2ProviderService.REQUEST_ID_UNKNOWN) {
+                // The session is created without any matching request.
+                // TODO: Tell managers for the session creation
+                return;
+            }
+
             SessionCreationRequest matchingRequest = null;
 
             for (SessionCreationRequest request : mSessionCreationRequests) {
@@ -1182,6 +1195,30 @@
             // TODO: Tell managers for the session creation
         }
 
+        private void onSessionCreationFailedOnHandler(@NonNull MediaRoute2Provider provider,
+                long 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 failed result for unknown request. "
+                        + "requestId=" + requestId);
+                return;
+            }
+
+            mSessionCreationRequests.remove(matchingRequest);
+            notifySessionCreationFailed(matchingRequest.mClientRecord,
+                    toClientRequestId(requestId));
+        }
+
         private void onSessionInfoChangedOnHandler(@NonNull MediaRoute2Provider provider,
                 @NonNull RoutingSessionInfo sessionInfo) {
 
diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
index 961d3d0..6695227 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
@@ -169,7 +169,7 @@
         for (MediaRoute2Info route : mBluetoothRoutes) {
             builder.addRoute(route);
         }
-        setProviderState(builder.build(), Collections.emptyList());
+        setProviderState(builder.build());
         mHandler.post(() -> notifyProviderState());
     }
 
@@ -212,6 +212,6 @@
         for (MediaRoute2Info route : mBluetoothRoutes) {
             builder.addRoute(route);
         }
-        setAndNotifyProviderState(builder.build(), Collections.emptyList());
+        setAndNotifyProviderState(builder.build());
     }
 }