Media : Implement getAvailableRoutes in MediaRouter2Manager

This CL adds capability to media route 2 info, which can be used
to get available routes for each app, which set control categories.

The test for control category is changed to test getAvailableRoutes().

Test: atest mediaroutertest

Change-Id: If93d64f02b4868b5e04b737431291b18a52177de
diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java
index f13a64c..5dcbb05 100644
--- a/media/java/android/media/MediaRoute2Info.java
+++ b/media/java/android/media/MediaRoute2Info.java
@@ -23,6 +23,9 @@
 import android.os.Parcelable;
 import android.text.TextUtils;
 
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
 import java.util.Objects;
 
 /**
@@ -53,6 +56,8 @@
     final String mDescription;
     @Nullable
     final String mClientPackageName;
+    @NonNull
+    final List<String> mSupportedCategories;
     @Nullable
     final Bundle mExtras;
 
@@ -62,6 +67,7 @@
         mName = builder.mName;
         mDescription = builder.mDescription;
         mClientPackageName = builder.mClientPackageName;
+        mSupportedCategories = builder.mSupportedCategories;
         mExtras = builder.mExtras;
     }
 
@@ -71,6 +77,7 @@
         mName = in.readString();
         mDescription = in.readString();
         mClientPackageName = in.readString();
+        mSupportedCategories = in.createStringArrayList();
         mExtras = in.readBundle();
     }
 
@@ -103,13 +110,14 @@
                 && Objects.equals(mName, other.mName)
                 && Objects.equals(mDescription, other.mDescription)
                 && Objects.equals(mClientPackageName, other.mClientPackageName)
+                && Objects.equals(mSupportedCategories, other.mSupportedCategories)
                 //TODO: This will be evaluated as false in most cases. Try not to.
                 && Objects.equals(mExtras, other.mExtras);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mId, mName, mDescription);
+        return Objects.hash(mId, mName, mDescription, mSupportedCategories);
     }
 
     @NonNull
@@ -146,11 +154,38 @@
         return mClientPackageName;
     }
 
+    /**
+     * Gets the supported categories of the route.
+     */
+    @NonNull
+    public List<String> getSupportedCategories() {
+        return mSupportedCategories;
+    }
+
     @Nullable
     public Bundle getExtras() {
         return mExtras;
     }
 
+    //TODO: Move this if we re-define control category / selector things.
+    /**
+     * Returns true if the route supports at least one of the specified control categories
+     *
+     * @param controlCategories the list of control categories to consider
+     * @return true if the route supports at least one category
+     */
+    public boolean supportsControlCategory(@NonNull Collection<String> controlCategories) {
+        Objects.requireNonNull(controlCategories, "control categories must not be null");
+        for (String controlCategory : controlCategories) {
+            for (String supportedCategory : getSupportedCategories()) {
+                if (TextUtils.equals(controlCategory, supportedCategory)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -163,6 +198,7 @@
         dest.writeString(mName);
         dest.writeString(mDescription);
         dest.writeString(mClientPackageName);
+        dest.writeStringList(mSupportedCategories);
         dest.writeBundle(mExtras);
     }
 
@@ -187,6 +223,7 @@
         String mName;
         String mDescription;
         String mClientPackageName;
+        List<String> mSupportedCategories;
         Bundle mExtras;
 
         public Builder(@NonNull String id, @NonNull String name) {
@@ -198,6 +235,7 @@
             }
             setId(id);
             setName(name);
+            mSupportedCategories = new ArrayList<>();
         }
 
         public Builder(@NonNull MediaRoute2Info routeInfo) {
@@ -212,6 +250,7 @@
             setName(routeInfo.mName);
             mDescription = routeInfo.mDescription;
             setClientPackageName(routeInfo.mClientPackageName);
+            setSupportedCategories(routeInfo.mSupportedCategories);
             if (routeInfo.mExtras != null) {
                 mExtras = new Bundle(routeInfo.mExtras);
             }
@@ -273,6 +312,39 @@
         }
 
         /**
+         * Sets the supported categories of the route.
+         */
+        @NonNull
+        public Builder setSupportedCategories(@NonNull Collection<String> categories) {
+            mSupportedCategories = new ArrayList<>();
+            return addSupportedCategories(categories);
+        }
+
+        /**
+         * Adds supported categories for the route.
+         */
+        @NonNull
+        public Builder addSupportedCategories(@NonNull Collection<String> categories) {
+            Objects.requireNonNull(categories, "categories must not be null");
+            for (String category: categories) {
+                addSupportedCategory(category);
+            }
+            return this;
+        }
+
+        /**
+         * Add a supported category for the route.
+         */
+        @NonNull
+        public Builder addSupportedCategory(@NonNull String category) {
+            if (TextUtils.isEmpty(category)) {
+                throw new IllegalArgumentException("category must not be null or empty");
+            }
+            mSupportedCategories.add(category);
+            return this;
+        }
+
+        /**
          * Sets a bundle of extras for the route.
          */
         @NonNull
diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java
index 5fc37a5..85105e0 100644
--- a/media/java/android/media/MediaRouter2Manager.java
+++ b/media/java/android/media/MediaRouter2Manager.java
@@ -38,6 +38,8 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.Executor;
 
@@ -66,6 +68,8 @@
     List<MediaRoute2ProviderInfo> mProviders = Collections.emptyList();
     @NonNull
     List<MediaRoute2Info> mRoutes = Collections.emptyList();
+    @NonNull
+    ConcurrentMap<String, List<String>> mControlCategoryMap = new ConcurrentHashMap<>();
 
     /**
      * Gets an instance of media router manager that controls media route of other applications.
@@ -160,6 +164,8 @@
         return -1;
     }
 
+    //TODO: Use cache not to create array. For now, it's unclear when to purge the cache.
+    //Do this when we finalize how to set control categories.
     /**
      * Gets available routes for an application.
      *
@@ -167,8 +173,17 @@
      */
     @NonNull
     public List<MediaRoute2Info> getAvailableRoutes(@NonNull String packageName) {
-        //TODO: filter irrelevant routes.
-        return Collections.unmodifiableList(mRoutes);
+        List<String> controlCategories = mControlCategoryMap.get(packageName);
+        if (controlCategories == null) {
+            return Collections.emptyList();
+        }
+        List<MediaRoute2Info> routes = new ArrayList<>();
+        for (MediaRoute2Info route : mRoutes) {
+            if (route.supportsControlCategory(controlCategories)) {
+                routes.add(route);
+            }
+        }
+        return routes;
     }
 
     /**
@@ -310,11 +325,8 @@
         }
     }
 
-    void notifyControlCategoriesChanged(String packageName, List<String> categories) {
-        for (CallbackRecord record : mCallbacks) {
-            record.mExecutor.execute(
-                    () -> record.mCallback.onControlCategoriesChanged(packageName, categories));
-        }
+    void updateControlCategories(String packageName, List<String> categories) {
+        mControlCategoryMap.put(packageName, categories);
     }
 
     /**
@@ -346,15 +358,6 @@
         public void onRouteSelected(@NonNull String packageName, @Nullable MediaRoute2Info route) {}
 
         /**
-         * Called when the control categories of an application is changed.
-         *
-         * @param packageName the package name of the app that changed control categories
-         * @param categories the changed categories
-         */
-        public void onControlCategoriesChanged(@NonNull String packageName,
-                @NonNull List<String> categories) {}
-
-        /**
          * Called when the list of routes are changed.
          * A client may refresh available routes for each application.
          */
@@ -389,7 +392,7 @@
 
         @Override
         public void notifyControlCategoriesChanged(String packageName, List<String> categories) {
-            mHandler.sendMessage(obtainMessage(MediaRouter2Manager::notifyControlCategoriesChanged,
+            mHandler.sendMessage(obtainMessage(MediaRouter2Manager::updateControlCategories,
                     MediaRouter2Manager.this, packageName, categories));
         }
 
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 2cdc6a8..1267aa8 100644
--- a/media/tests/MediaRouteProvider/src/com/android/mediarouteprovider/example/SampleMediaRoute2ProviderService.java
+++ b/media/tests/MediaRouteProvider/src/com/android/mediarouteprovider/example/SampleMediaRoute2ProviderService.java
@@ -32,19 +32,35 @@
     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_ID_SPECIAL_CATEGORY = "route_special_category";
+    public static final String ROUTE_NAME_SPECIAL_CATEGORY = "Special Category Route";
+
     public static final String ACTION_REMOVE_ROUTE =
             "com.android.mediarouteprovider.action_remove_route";
 
+    public static final String CATEGORY_SAMPLE =
+            "com.android.mediarouteprovider.CATEGORY_SAMPLE";
+    public static final String CATEGORY_SPECIAL =
+            "com.android.mediarouteprovider.CATEGORY_SPECIAL";
+
     Map<String, MediaRoute2Info> mRoutes = new HashMap<>();
 
     private void initializeRoutes() {
         MediaRoute2Info route1 = new MediaRoute2Info.Builder(ROUTE_ID1, ROUTE_NAME1)
+                .addSupportedCategory(CATEGORY_SAMPLE)
                 .build();
         MediaRoute2Info route2 = new MediaRoute2Info.Builder(ROUTE_ID2, ROUTE_NAME2)
+                .addSupportedCategory(CATEGORY_SAMPLE)
                 .build();
-
+        MediaRoute2Info routeSpecial =
+                new MediaRoute2Info.Builder(ROUTE_ID_SPECIAL_CATEGORY, ROUTE_NAME_SPECIAL_CATEGORY)
+                        .addSupportedCategory(CATEGORY_SAMPLE)
+                        .addSupportedCategory(CATEGORY_SPECIAL)
+                        .build();
         mRoutes.put(route1.getId(), route1);
         mRoutes.put(route2.getId(), route2);
+        mRoutes.put(routeSpecial.getId(), routeSpecial);
     }
 
     @Override
diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java
index 03b43e2..4282a5bc 100644
--- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java
+++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouterManagerTest.java
@@ -58,9 +58,18 @@
     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_ID_SPECIAL_CATEGORY = "route_special_category";
+    public static final String ROUTE_NAME_SPECIAL_CATEGORY = "Special Category Route";
+
     public static final String ACTION_REMOVE_ROUTE =
             "com.android.mediarouteprovider.action_remove_route";
 
+    public static final String CATEGORY_SAMPLE =
+            "com.android.mediarouteprovider.CATEGORY_SAMPLE";
+    public static final String CATEGORY_SPECIAL =
+            "com.android.mediarouteprovider.CATEGORY_SPECIAL";
+
     private static final int TIMEOUT_MS = 5000;
 
     private Context mContext;
@@ -69,12 +78,13 @@
     private Executor mExecutor;
     private String mPackageName;
 
-    private static final List<String> TEST_CONTROL_CATEGORIES = new ArrayList();
-    private static final String CONTROL_CATEGORY_1 = "android.media.mediarouter.MEDIA1";
-    private static final String CONTROL_CATEGORY_2 = "android.media.mediarouter.MEDIA2";
+    private static final List<String> CONTROL_CATEGORIES_ALL = new ArrayList();
+    private static final List<String> CONTROL_CATEGORIES_SPECIAL = new ArrayList();
     static {
-        TEST_CONTROL_CATEGORIES.add(CONTROL_CATEGORY_1);
-        TEST_CONTROL_CATEGORIES.add(CONTROL_CATEGORY_2);
+        CONTROL_CATEGORIES_ALL.add(CATEGORY_SAMPLE);
+        CONTROL_CATEGORIES_ALL.add(CATEGORY_SPECIAL);
+
+        CONTROL_CATEGORIES_SPECIAL.add(CATEGORY_SPECIAL);
     }
 
     @Before
@@ -125,7 +135,7 @@
         // (Control requests shouldn't be used in this way.)
         InstrumentationRegistry.getInstrumentation().runOnMainSync(
                 (Runnable) () -> {
-                    mRouter.addCallback(TEST_CONTROL_CATEGORIES, mExecutor, mockRouterCallback);
+                    mRouter.addCallback(CONTROL_CATEGORIES_ALL, mExecutor, mockRouterCallback);
                     mRouter.sendControlRequest(
                             new MediaRoute2Info.Builder(ROUTE_ID2, ROUTE_NAME2).build(),
                             new Intent(ACTION_REMOVE_ROUTE));
@@ -138,8 +148,11 @@
         mManager.removeCallback(mockCallback);
     }
 
+    /**
+     * Tests if we get proper routes for application that has special control category.
+     */
     @Test
-    public void controlCategoryTest() throws Exception {
+    public void testControlCategory() throws Exception {
         MediaRouter2Manager.Callback mockCallback = mock(MediaRouter2Manager.Callback.class);
         mManager.addCallback(mExecutor, mockCallback);
 
@@ -147,12 +160,19 @@
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync(
                 () -> {
-                    mRouter.addCallback(TEST_CONTROL_CATEGORIES, mExecutor, mockRouterCallback);
+                    mRouter.addCallback(CONTROL_CATEGORIES_SPECIAL,
+                            mExecutor, mockRouterCallback);
                     mRouter.removeCallback(mockRouterCallback);
                 }
         );
-        verify(mockCallback, timeout(TIMEOUT_MS).atLeastOnce())
-                .onControlCategoriesChanged(mPackageName, TEST_CONTROL_CATEGORIES);
+        verify(mockCallback, timeout(TIMEOUT_MS))
+                .onRouteListChanged(argThat(routes -> routes.size() > 0));
+
+        Map<String, MediaRoute2Info> routes =
+                createRouteMap(mManager.getAvailableRoutes(mPackageName));
+
+        Assert.assertEquals(1, routes.size());
+        Assert.assertNotNull(routes.get(ROUTE_ID_SPECIAL_CATEGORY));
 
         mManager.removeCallback(mockCallback);
     }
@@ -164,7 +184,7 @@
         MediaRouter2.Callback mockRouterCallback = mock(MediaRouter2.Callback.class);
         InstrumentationRegistry.getInstrumentation().runOnMainSync(
                 () -> {
-                    mRouter.addCallback(TEST_CONTROL_CATEGORIES, mExecutor, mockRouterCallback);
+                    mRouter.addCallback(CONTROL_CATEGORIES_ALL, mExecutor, mockRouterCallback);
                 }
         );
 
@@ -197,10 +217,10 @@
         mManager.removeCallback(managerCallback);
     }
 
-    @Test
     /**
      * Tests selecting and unselecting routes of a single provider.
      */
+    @Test
     public void testSingleProviderSelect() {
         MediaRouter2Manager.Callback managerCallback = mock(MediaRouter2Manager.Callback.class);
         MediaRouter2.Callback routerCallback = mock(MediaRouter2.Callback.class);
@@ -208,7 +228,7 @@
         mManager.addCallback(mExecutor, managerCallback);
         InstrumentationRegistry.getInstrumentation().runOnMainSync(
                 () -> {
-                    mRouter.addCallback(TEST_CONTROL_CATEGORIES, mExecutor, routerCallback);
+                    mRouter.addCallback(CONTROL_CATEGORIES_ALL, mExecutor, routerCallback);
                 }
         );
         verify(managerCallback, timeout(TIMEOUT_MS))
@@ -218,7 +238,8 @@
                 createRouteMap(mManager.getAvailableRoutes(mPackageName));
 
         mManager.selectRoute(mPackageName, routes.get(ROUTE_ID1));
-        verify(managerCallback, timeout(TIMEOUT_MS))
+        verify(managerCallback, timeout(TIMEOUT_MS)
+        )
                 .onRouteChanged(argThat(routeInfo -> TextUtils.equals(ROUTE_ID1, routeInfo.getId())
                         && TextUtils.equals(routeInfo.getClientPackageName(), mPackageName)));