Merge "Media: Add group id for media router to sync" into qt-dev
diff --git a/media/java/android/media/IMediaRouterClient.aidl b/media/java/android/media/IMediaRouterClient.aidl
index 08344f1..240ae79 100644
--- a/media/java/android/media/IMediaRouterClient.aidl
+++ b/media/java/android/media/IMediaRouterClient.aidl
@@ -22,4 +22,5 @@
 oneway interface IMediaRouterClient {
     void onStateChanged();
     void onRestoreRoute();
+    void onSelectedRouteChanged(String routeId);
 }
diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl
index 3308fc9..04c2d07 100644
--- a/media/java/android/media/IMediaRouterService.aidl
+++ b/media/java/android/media/IMediaRouterService.aidl
@@ -26,6 +26,8 @@
     void registerClientAsUser(IMediaRouterClient client, String packageName, int userId);
     void unregisterClient(IMediaRouterClient client);
 
+    void registerClientGroupId(IMediaRouterClient client, String groupId);
+
     MediaRouterClientState getState(IMediaRouterClient client);
     boolean isPlaybackActive(IMediaRouterClient client);
 
diff --git a/media/java/android/media/MediaRouter.java b/media/java/android/media/MediaRouter.java
index 3444e92..d72231f 100644
--- a/media/java/android/media/MediaRouter.java
+++ b/media/java/android/media/MediaRouter.java
@@ -20,6 +20,7 @@
 import android.annotation.DrawableRes;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.SystemService;
 import android.annotation.UnsupportedAppUsage;
 import android.app.ActivityThread;
@@ -343,6 +344,16 @@
             updatePresentationDisplays(displayId);
         }
 
+        public void setRouterGroupId(String groupId) {
+            if (mClient != null) {
+                try {
+                    mMediaRouterService.registerClientGroupId(mClient, groupId);
+                } catch (RemoteException ex) {
+                    Log.e(TAG, "Unable to register group ID of the client.", ex);
+                }
+            }
+        }
+
         public Display[] getAllPresentationDisplays() {
             return mDisplayService.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION);
         }
@@ -358,6 +369,21 @@
             }
         }
 
+        void updateSelectedRouteForId(String routeId) {
+            RouteInfo selectedRoute = isBluetoothA2dpOn()
+                    ? mBluetoothA2dpRoute : mDefaultAudioVideo;
+            final int count = mRoutes.size();
+            for (int i = 0; i < count; i++) {
+                final RouteInfo route = mRoutes.get(i);
+                if (TextUtils.equals(route.mGlobalRouteId, routeId)) {
+                    selectedRoute = route;
+                }
+            }
+            if (selectedRoute != mSelectedRoute) {
+                selectRouteStatic(selectedRoute.mSupportedTypes, selectedRoute, false);
+            }
+        }
+
         void setSelectedRoute(RouteInfo info, boolean explicit) {
             // Must be non-reentrant.
             mSelectedRoute = info;
@@ -619,6 +645,15 @@
                     }
                 });
             }
+
+            @Override
+            public void onSelectedRouteChanged(String routeId) {
+                mHandler.post(() -> {
+                    if (Client.this == mClient) {
+                        updateSelectedRouteForId(routeId);
+                    }
+                });
+            }
         }
     }
 
@@ -728,6 +763,13 @@
      */
     public static final int AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE = 1 << 0;
 
+    /**
+     * The route group id used for sharing the selected mirroring device.
+     * System UI and Settings use this to synchronize their mirroring status.
+     * @hide
+     */
+    public static final String MIRRORING_GROUP_ID = "android.media.mirroring_group";
+
     // Maps application contexts
     static final HashMap<Context, MediaRouter> sRouters = new HashMap<Context, MediaRouter>();
 
@@ -848,6 +890,25 @@
     }
 
     /**
+     * Sets the group ID of the router.
+     * Media routers with the same ID acts as if they were a single media router.
+     * For example, if a media router selects a route, the selected route of routers
+     * with the same group ID will be changed automatically.
+     *
+     * Two routers in a group are supposed to use the same route types.
+     *
+     * System UI and Settings use this to synchronize their mirroring status.
+     * Do not set the router group id unless it's necessary.
+     *
+     * {@link android.Manifest.permission#CONFIGURE_WIFI_DISPLAY} permission is required to
+     * call this method.
+     * @hide
+     */
+    public void setRouterGroupId(@Nullable String groupId) {
+        sStatic.setRouterGroupId(groupId);
+    }
+
+    /**
      * Add a callback to listen to events about specific kinds of media routes.
      * If the specified callback is already registered, its registration will be updated for any
      * additional route types specified.
diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java
index 23d3ce0..7b9fff5 100644
--- a/services/core/java/com/android/server/media/MediaRouterService.java
+++ b/services/core/java/com/android/server/media/MediaRouterService.java
@@ -248,6 +248,29 @@
 
     // Binder call
     @Override
+    public void registerClientGroupId(IMediaRouterClient client, String groupId) {
+        if (client == null) {
+            throw new NullPointerException("client must not be null");
+        }
+        if (mContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.CONFIGURE_WIFI_DISPLAY)
+                != PackageManager.PERMISSION_GRANTED) {
+            Log.w(TAG, "Ignoring client group request because "
+                    + "the client doesn't have the CONFIGURE_WIFI_DISPLAY permission.");
+            return;
+        }
+        final long token = Binder.clearCallingIdentity();
+        try {
+            synchronized (mLock) {
+                registerClientGroupIdLocked(client, groupId);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    // Binder call
+    @Override
     public void unregisterClient(IMediaRouterClient client) {
         if (client == null) {
             throw new IllegalArgumentException("client must not be null");
@@ -502,11 +525,37 @@
         }
     }
 
+    private void registerClientGroupIdLocked(IMediaRouterClient client, String groupId) {
+        final IBinder binder = client.asBinder();
+        ClientRecord clientRecord = mAllClientRecords.get(binder);
+        if (clientRecord == null) {
+            Log.w(TAG, "Ignoring group id register request of a unregistered client.");
+            return;
+        }
+        if (TextUtils.equals(clientRecord.mGroupId, groupId)) {
+            return;
+        }
+        UserRecord userRecord = clientRecord.mUserRecord;
+        if (clientRecord.mGroupId != null) {
+            userRecord.removeFromGroup(clientRecord.mGroupId, clientRecord);
+        }
+        clientRecord.mGroupId = groupId;
+        if (groupId != null) {
+            userRecord.addToGroup(groupId, clientRecord);
+            userRecord.mHandler.obtainMessage(UserHandler.MSG_UPDATE_SELECTED_ROUTE, groupId)
+                .sendToTarget();
+        }
+    }
+
     private void unregisterClientLocked(IMediaRouterClient client, boolean died) {
         ClientRecord clientRecord = mAllClientRecords.remove(client.asBinder());
         if (clientRecord != null) {
             UserRecord userRecord = clientRecord.mUserRecord;
             userRecord.mClientRecords.remove(clientRecord);
+            if (clientRecord.mGroupId != null) {
+                userRecord.removeFromGroup(clientRecord.mGroupId, clientRecord);
+                clientRecord.mGroupId = null;
+            }
             disposeClientLocked(clientRecord, died);
             disposeUserIfNeededLocked(userRecord); // since client removed from user
         }
@@ -568,6 +617,16 @@
                         clientRecord.mUserRecord.mHandler.obtainMessage(
                                 UserHandler.MSG_SELECT_ROUTE, routeId).sendToTarget();
                     }
+                    if (clientRecord.mGroupId != null) {
+                        ClientGroup group =
+                                clientRecord.mUserRecord.mClientGroupMap.get(clientRecord.mGroupId);
+                        if (group != null) {
+                            group.mSelectedRouteId = routeId;
+                            clientRecord.mUserRecord.mHandler.obtainMessage(
+                                UserHandler.MSG_UPDATE_SELECTED_ROUTE, clientRecord.mGroupId)
+                                .sendToTarget();
+                        }
+                    }
                 }
             }
         }
@@ -680,6 +739,7 @@
         public int mRouteTypes;
         public boolean mActiveScan;
         public String mSelectedRouteId;
+        public String mGroupId;
 
         public ClientRecord(UserRecord userRecord, IMediaRouterClient client,
                 int uid, int pid, String packageName, boolean trusted) {
@@ -720,6 +780,11 @@
         }
     }
 
+    final class ClientGroup {
+        public String mSelectedRouteId;
+        public final List<ClientRecord> mClientRecords = new ArrayList<>();
+    }
+
     /**
      * Information about a particular user.
      * The contents of this object is guarded by mLock.
@@ -729,6 +794,7 @@
         public final ArrayList<ClientRecord> mClientRecords = new ArrayList<ClientRecord>();
         public final UserHandler mHandler;
         public MediaRouterClientState mRouterState;
+        private final ArrayMap<String, ClientGroup> mClientGroupMap = new ArrayMap<>();
 
         public UserRecord(int userId) {
             mUserId = userId;
@@ -759,7 +825,26 @@
             }, 1000)) {
                 pw.println(indent + "<could not dump handler state>");
             }
-         }
+        }
+
+        public void addToGroup(String groupId, ClientRecord clientRecord) {
+            ClientGroup group = mClientGroupMap.get(groupId);
+            if (group == null) {
+                group = new ClientGroup();
+                mClientGroupMap.put(groupId, group);
+            }
+            group.mClientRecords.add(clientRecord);
+        }
+
+        public void removeFromGroup(String groupId, ClientRecord clientRecord) {
+            ClientGroup group = mClientGroupMap.get(groupId);
+            if (group != null) {
+                group.mClientRecords.remove(clientRecord);
+                if (group.mClientRecords.size() == 0) {
+                    mClientGroupMap.remove(groupId);
+                }
+            }
+        }
 
         @Override
         public String toString() {
@@ -791,6 +876,7 @@
         public static final int MSG_REQUEST_UPDATE_VOLUME = 7;
         private static final int MSG_UPDATE_CLIENT_STATE = 8;
         private static final int MSG_CONNECTION_TIMED_OUT = 9;
+        private static final int MSG_UPDATE_SELECTED_ROUTE = 10;
 
         private static final int TIMEOUT_REASON_NOT_AVAILABLE = 1;
         private static final int TIMEOUT_REASON_CONNECTION_LOST = 2;
@@ -867,6 +953,10 @@
                     connectionTimedOut();
                     break;
                 }
+                case MSG_UPDATE_SELECTED_ROUTE: {
+                    updateSelectedRoute((String) msg.obj);
+                    break;
+                }
             }
         }
 
@@ -1191,6 +1281,37 @@
             }
         }
 
+        private void updateSelectedRoute(String groupId) {
+            try {
+                String selectedRouteId = null;
+                synchronized (mService.mLock) {
+                    ClientGroup group = mUserRecord.mClientGroupMap.get(groupId);
+                    if (group == null) {
+                        return;
+                    }
+                    selectedRouteId = group.mSelectedRouteId;
+                    final int count = group.mClientRecords.size();
+                    for (int i = 0; i < count; i++) {
+                        ClientRecord clientRecord = group.mClientRecords.get(i);
+                        if (!TextUtils.equals(selectedRouteId, clientRecord.mSelectedRouteId)) {
+                            mTempClients.add(clientRecord.mClient);
+                        }
+                    }
+                }
+
+                final int count = mTempClients.size();
+                for (int i = 0; i < count; i++) {
+                    try {
+                        mTempClients.get(i).onSelectedRouteChanged(selectedRouteId);
+                    } catch (RemoteException ex) {
+                        Slog.w(TAG, "Failed to call onSelectedRouteChanged. Client probably died.");
+                    }
+                }
+            } finally {
+                mTempClients.clear();
+            }
+        }
+
         private int findProviderRecord(RemoteDisplayProviderProxy provider) {
             final int count = mProviderRecords.size();
             for (int i = 0; i < count; i++) {