[Output Switcher] Add group title

Apply "Display & Speaker" group in the list, it should contain all the
devices besides selected device when launch.
Refine volume control

Bug: 255124239
Test: verified on device
Change-Id: I5467e9bc25ca3685830bee2ce579311336ec2331
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 80e969e..7b1c6c6 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2406,6 +2406,8 @@
     <string name="media_output_dialog_accessibility_seekbar">Volume</string>
     <!-- Summary for media output volume of a device in percentage [CHAR LIMIT=NONE] -->
     <string name="media_output_dialog_volume_percentage"><xliff:g id="percentage" example="10">%1$d</xliff:g>%%</string>
+    <!-- Title for Speakers and Displays group. [CHAR LIMIT=NONE] -->
+    <string name="media_output_group_title_speakers_and_displays">Speakers &amp; Displays</string>
 
     <!-- Media Output Broadcast Dialog -->
     <!-- Title for Broadcast First Notify Dialog [CHAR LIMIT=60] -->
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index fb47d97..51b5a3d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -70,6 +70,13 @@
     @Override
     public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
         if (mController.isAdvancedLayoutSupported()) {
+            if (position >= mController.getMediaItemList().size()) {
+                if (DEBUG) {
+                    Log.d(TAG, "Incorrect position: " + position + " list size: "
+                            + mController.getMediaItemList().size());
+                }
+                return;
+            }
             MediaItem currentMediaItem = mController.getMediaItemList().get(position);
             switch (currentMediaItem.getMediaItemType()) {
                 case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER:
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index 3b1d861..4e08050 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -84,8 +84,7 @@
             int viewType) {
         mContext = viewGroup.getContext();
         mHolderView = LayoutInflater.from(mContext).inflate(
-                mController.isAdvancedLayoutSupported() ? MediaItem.getMediaLayoutId(
-                        viewType) /*R.layout.media_output_list_item_advanced*/
+                mController.isAdvancedLayoutSupported() ? MediaItem.getMediaLayoutId(viewType)
                         : R.layout.media_output_list_item, viewGroup, false);
 
         return null;
@@ -308,9 +307,10 @@
                         updateTitleIcon(currentVolume == 0 ? R.drawable.media_output_icon_volume_off
                                         : R.drawable.media_output_icon_volume,
                                 mController.getColorItemContent());
+                    } else {
+                        animateCornerAndVolume(mSeekBar.getProgress(),
+                                MediaOutputSeekbar.scaleVolumeToProgress(currentVolume));
                     }
-                    animateCornerAndVolume(mSeekBar.getProgress(),
-                            MediaOutputSeekbar.scaleVolumeToProgress(currentVolume));
                 } else {
                     if (!mVolumeAnimator.isStarted()) {
                         if (mController.isAdvancedLayoutSupported()) {
@@ -396,23 +396,18 @@
         }
 
         void updateMutedVolumeIcon() {
+            mIconAreaLayout.setBackground(
+                    mContext.getDrawable(R.drawable.media_output_item_background_active));
             updateTitleIcon(R.drawable.media_output_icon_volume_off,
                     mController.getColorItemContent());
-            final GradientDrawable iconAreaBackgroundDrawable =
-                    (GradientDrawable) mIconAreaLayout.getBackground();
-            iconAreaBackgroundDrawable.setCornerRadius(mController.getActiveRadius());
         }
 
         void updateUnmutedVolumeIcon() {
+            mIconAreaLayout.setBackground(
+                    mContext.getDrawable(R.drawable.media_output_title_icon_area)
+            );
             updateTitleIcon(R.drawable.media_output_icon_volume,
                     mController.getColorItemContent());
-            final GradientDrawable iconAreaBackgroundDrawable =
-                    (GradientDrawable) mIconAreaLayout.getBackground();
-            iconAreaBackgroundDrawable.setCornerRadii(new float[]{
-                    mController.getActiveRadius(),
-                    mController.getActiveRadius(),
-                    0, 0, 0, 0, mController.getActiveRadius(), mController.getActiveRadius()
-            });
         }
 
         void updateTitleIcon(@DrawableRes int id, int color) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index b436562..8eb25c4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -87,9 +87,11 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.Executor;
@@ -275,7 +277,9 @@
 
     @Override
     public void onDeviceListUpdate(List<MediaDevice> devices) {
-        if (mMediaDevices.isEmpty() || !mIsRefreshing) {
+        boolean isListEmpty =
+                isAdvancedLayoutSupported() ? mMediaItemList.isEmpty() : mMediaDevices.isEmpty();
+        if (isListEmpty || !mIsRefreshing) {
             buildMediaDevices(devices);
             mCallback.onDeviceListChanged();
         } else {
@@ -646,16 +650,19 @@
                         for (MediaDevice device : devices) {
                             if (device.isMutingExpectedDevice()) {
                                 mMediaItemList.add(0, new MediaItem(device));
+                                mMediaItemList.add(1, new MediaItem(mContext.getString(
+                                        R.string.media_output_group_title_speakers_and_displays),
+                                        MediaItem.MediaItemType.TYPE_GROUP_DIVIDER));
                             } else {
                                 mMediaItemList.add(new MediaItem(device));
                             }
                         }
+                        mMediaItemList.add(new MediaItem());
                     } else {
                         mMediaItemList.addAll(
                                 devices.stream().map(MediaItem::new).collect(Collectors.toList()));
+                        categorizeMediaItems(null);
                     }
-
-                    categorizeMediaItems();
                     return;
                 }
                 // selected device exist
@@ -666,11 +673,12 @@
                         mMediaItemList.add(new MediaItem(device));
                     }
                 }
-                categorizeMediaItems();
+                categorizeMediaItems(connectedMediaDevice);
                 return;
             }
             // To keep the same list order
             final List<MediaDevice> targetMediaDevices = new ArrayList<>();
+            final Map<Integer, MediaItem> dividerItems = new HashMap<>();
             for (MediaItem originalMediaItem : mMediaItemList) {
                 for (MediaDevice newDevice : devices) {
                     if (originalMediaItem.getMediaDevice().isPresent()
@@ -680,6 +688,10 @@
                         break;
                     }
                 }
+                if (originalMediaItem.getMediaItemType()
+                        == MediaItem.MediaItemType.TYPE_GROUP_DIVIDER) {
+                    dividerItems.put(mMediaItemList.indexOf(originalMediaItem), originalMediaItem);
+                }
             }
             if (targetMediaDevices.size() != devices.size()) {
                 devices.removeAll(targetMediaDevices);
@@ -688,13 +700,34 @@
             mMediaItemList.clear();
             mMediaItemList.addAll(
                     targetMediaDevices.stream().map(MediaItem::new).collect(Collectors.toList()));
-            categorizeMediaItems();
+            dividerItems.forEach((key, item) -> {
+                mMediaItemList.add(key, item);
+            });
+            mMediaItemList.add(new MediaItem());
         }
     }
 
-    private void categorizeMediaItems() {
+    private void categorizeMediaItems(MediaDevice connectedMediaDevice) {
         synchronized (mMediaDevicesLock) {
-            //TODO(255124239): do the categorization here
+            Set<String> selectedDevicesIds = getSelectedMediaDevice().stream().map(
+                    MediaDevice::getId).collect(Collectors.toSet());
+            if (connectedMediaDevice != null) {
+                selectedDevicesIds.add(connectedMediaDevice.getId());
+            }
+            int latestSelected = 1;
+            for (MediaItem item : mMediaItemList) {
+                if (item.getMediaDevice().isPresent()) {
+                    MediaDevice device = item.getMediaDevice().get();
+                    if (selectedDevicesIds.contains(device.getId())) {
+                        latestSelected = mMediaItemList.indexOf(item) + 1;
+                    } else {
+                        mMediaItemList.add(latestSelected, new MediaItem(mContext.getString(
+                                R.string.media_output_group_title_speakers_and_displays),
+                                MediaItem.MediaItemType.TYPE_GROUP_DIVIDER));
+                        break;
+                    }
+                }
+            }
             mMediaItemList.add(new MediaItem());
         }
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
index 71c300c..b16a39f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
@@ -102,6 +102,8 @@
     private MediaOutputController.Callback mCb = mock(MediaOutputController.Callback.class);
     private MediaDevice mMediaDevice1 = mock(MediaDevice.class);
     private MediaDevice mMediaDevice2 = mock(MediaDevice.class);
+    private MediaItem mMediaItem1 = mock(MediaItem.class);
+    private MediaItem mMediaItem2 = mock(MediaItem.class);
     private NearbyDevice mNearbyDevice1 = mock(NearbyDevice.class);
     private NearbyDevice mNearbyDevice2 = mock(NearbyDevice.class);
     private MediaMetadata mMediaMetadata = mock(MediaMetadata.class);
@@ -125,6 +127,7 @@
     private LocalMediaManager mLocalMediaManager;
     private List<MediaController> mMediaControllers = new ArrayList<>();
     private List<MediaDevice> mMediaDevices = new ArrayList<>();
+    private List<MediaItem> mMediaItemList = new ArrayList<>();
     private List<NearbyDevice> mNearbyDevices = new ArrayList<>();
     private MediaDescription mMediaDescription;
     private List<RoutingSessionInfo> mRoutingSessionInfos = new ArrayList<>();
@@ -157,6 +160,11 @@
         when(mMediaDevice2.getId()).thenReturn(TEST_DEVICE_2_ID);
         mMediaDevices.add(mMediaDevice1);
         mMediaDevices.add(mMediaDevice2);
+        when(mMediaItem1.getMediaDevice()).thenReturn(Optional.of(mMediaDevice1));
+        when(mMediaItem2.getMediaDevice()).thenReturn(Optional.of(mMediaDevice2));
+        mMediaItemList.add(mMediaItem1);
+        mMediaItemList.add(mMediaItem2);
+
 
         when(mNearbyDevice1.getMediaRoute2Id()).thenReturn(TEST_DEVICE_1_ID);
         when(mNearbyDevice1.getRangeZone()).thenReturn(NearbyDevice.RANGE_CLOSE);
@@ -314,6 +322,18 @@
     }
 
     @Test
+    public void advanced_onDeviceListUpdate_isRefreshing_updatesNeedRefreshToTrue() {
+        when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(true);
+        mMediaOutputController.start(mCb);
+        reset(mCb);
+        mMediaOutputController.mIsRefreshing = true;
+
+        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+
+        assertThat(mMediaOutputController.mNeedRefresh).isTrue();
+    }
+
+    @Test
     public void cancelMuteAwaitConnection_cancelsWithMediaManager() {
         when(mAudioManager.getMutingExpectedDevice()).thenReturn(mock(AudioDeviceAttributes.class));
         mMediaOutputController.start(mCb);