1 /* 2 * Copyright (C) 2025 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.media.dialog; 18 19 import android.content.Context; 20 21 import androidx.annotation.Nullable; 22 23 import com.android.media.flags.Flags; 24 import com.android.settingslib.media.MediaDevice; 25 import com.android.systemui.res.R; 26 27 import java.util.ArrayList; 28 import java.util.HashMap; 29 import java.util.HashSet; 30 import java.util.List; 31 import java.util.Map; 32 import java.util.Optional; 33 import java.util.Set; 34 import java.util.concurrent.CopyOnWriteArrayList; 35 import java.util.stream.Collectors; 36 37 /** A proxy of holding the list of Output Switcher's output media items. */ 38 public class OutputMediaItemListProxy { 39 private final Context mContext; 40 private final List<MediaItem> mOutputMediaItemList; 41 42 // Use separated lists to hold different media items and create the list of output media items 43 // by using those separated lists and group dividers. 44 private final List<MediaItem> mSelectedMediaItems; 45 private final List<MediaItem> mSuggestedMediaItems; 46 private final List<MediaItem> mSpeakersAndDisplaysMediaItems; 47 OutputMediaItemListProxy(Context context)48 public OutputMediaItemListProxy(Context context) { 49 mContext = context; 50 mOutputMediaItemList = new CopyOnWriteArrayList<>(); 51 mSelectedMediaItems = new CopyOnWriteArrayList<>(); 52 mSuggestedMediaItems = new CopyOnWriteArrayList<>(); 53 mSpeakersAndDisplaysMediaItems = new CopyOnWriteArrayList<>(); 54 } 55 56 /** Returns the list of output media items. */ getOutputMediaItemList()57 public List<MediaItem> getOutputMediaItemList() { 58 if (Flags.fixOutputMediaItemListIndexOutOfBoundsException()) { 59 if (isEmpty() && !mOutputMediaItemList.isEmpty()) { 60 // Ensures mOutputMediaItemList is empty when all individual media item lists are 61 // empty, preventing unexpected state issues. 62 mOutputMediaItemList.clear(); 63 } else if (!isEmpty() && mOutputMediaItemList.isEmpty()) { 64 // When any individual media item list is modified, the cached mOutputMediaItemList 65 // is emptied. On the next request for the output media item list, a fresh list is 66 // created and stored in the cache. 67 mOutputMediaItemList.addAll(createOutputMediaItemList()); 68 } 69 } 70 return mOutputMediaItemList; 71 } 72 createOutputMediaItemList()73 private List<MediaItem> createOutputMediaItemList() { 74 List<MediaItem> finalMediaItems = new CopyOnWriteArrayList<>(); 75 finalMediaItems.addAll(mSelectedMediaItems); 76 if (!mSuggestedMediaItems.isEmpty()) { 77 finalMediaItems.add( 78 MediaItem.createGroupDividerMediaItem( 79 mContext.getString( 80 R.string.media_output_group_title_suggested_device))); 81 finalMediaItems.addAll(mSuggestedMediaItems); 82 } 83 if (!mSpeakersAndDisplaysMediaItems.isEmpty()) { 84 finalMediaItems.add( 85 MediaItem.createGroupDividerMediaItem( 86 mContext.getString( 87 R.string.media_output_group_title_speakers_and_displays))); 88 finalMediaItems.addAll(mSpeakersAndDisplaysMediaItems); 89 } 90 return finalMediaItems; 91 } 92 93 /** Updates the list of output media items with a given list of media devices. */ updateMediaDevices( List<MediaDevice> devices, List<MediaDevice> selectedDevices, @Nullable MediaDevice connectedMediaDevice, boolean needToHandleMutingExpectedDevice)94 public void updateMediaDevices( 95 List<MediaDevice> devices, 96 List<MediaDevice> selectedDevices, 97 @Nullable MediaDevice connectedMediaDevice, 98 boolean needToHandleMutingExpectedDevice) { 99 Set<String> selectedOrConnectedMediaDeviceIds = 100 selectedDevices.stream().map(MediaDevice::getId).collect(Collectors.toSet()); 101 if (connectedMediaDevice != null) { 102 selectedOrConnectedMediaDeviceIds.add(connectedMediaDevice.getId()); 103 } 104 105 List<MediaItem> selectedMediaItems = new ArrayList<>(); 106 List<MediaItem> suggestedMediaItems = new ArrayList<>(); 107 List<MediaItem> speakersAndDisplaysMediaItems = new ArrayList<>(); 108 Map<String, MediaItem> deviceIdToMediaItemMap = new HashMap<>(); 109 buildMediaItems( 110 devices, 111 selectedOrConnectedMediaDeviceIds, 112 needToHandleMutingExpectedDevice, 113 selectedMediaItems, 114 suggestedMediaItems, 115 speakersAndDisplaysMediaItems, 116 deviceIdToMediaItemMap); 117 118 List<MediaItem> updatedSelectedMediaItems = new CopyOnWriteArrayList<>(); 119 List<MediaItem> updatedSuggestedMediaItems = new CopyOnWriteArrayList<>(); 120 List<MediaItem> updatedSpeakersAndDisplaysMediaItems = new CopyOnWriteArrayList<>(); 121 if (isEmpty()) { 122 updatedSelectedMediaItems.addAll(selectedMediaItems); 123 updatedSuggestedMediaItems.addAll(suggestedMediaItems); 124 updatedSpeakersAndDisplaysMediaItems.addAll(speakersAndDisplaysMediaItems); 125 } else { 126 Set<String> updatedDeviceIds = new HashSet<>(); 127 // Preserve the existing media item order while updating with the latest device 128 // information. Some items may retain their original group (suggested, speakers and 129 // displays) to maintain this order. 130 updateMediaItems( 131 mSelectedMediaItems, 132 updatedSelectedMediaItems, 133 deviceIdToMediaItemMap, 134 updatedDeviceIds); 135 updateMediaItems( 136 mSuggestedMediaItems, 137 updatedSuggestedMediaItems, 138 deviceIdToMediaItemMap, 139 updatedDeviceIds); 140 updateMediaItems( 141 mSpeakersAndDisplaysMediaItems, 142 updatedSpeakersAndDisplaysMediaItems, 143 deviceIdToMediaItemMap, 144 updatedDeviceIds); 145 146 // Append new media items that are not already in the existing lists to the output list. 147 List<MediaItem> remainingMediaItems = new ArrayList<>(); 148 remainingMediaItems.addAll( 149 getRemainingMediaItems(selectedMediaItems, updatedDeviceIds)); 150 remainingMediaItems.addAll( 151 getRemainingMediaItems(suggestedMediaItems, updatedDeviceIds)); 152 remainingMediaItems.addAll( 153 getRemainingMediaItems(speakersAndDisplaysMediaItems, updatedDeviceIds)); 154 updatedSpeakersAndDisplaysMediaItems.addAll(remainingMediaItems); 155 } 156 157 if (Flags.enableOutputSwitcherDeviceGrouping() && !updatedSelectedMediaItems.isEmpty()) { 158 MediaItem selectedMediaItem = updatedSelectedMediaItems.get(0); 159 Optional<MediaDevice> mediaDeviceOptional = selectedMediaItem.getMediaDevice(); 160 if (mediaDeviceOptional.isPresent()) { 161 MediaItem updatedMediaItem = 162 MediaItem.createDeviceMediaItem( 163 mediaDeviceOptional.get(), /* isFirstDeviceInGroup= */ true); 164 updatedSelectedMediaItems.remove(0); 165 updatedSelectedMediaItems.add(0, updatedMediaItem); 166 } 167 } 168 169 mSelectedMediaItems.clear(); 170 mSelectedMediaItems.addAll(updatedSelectedMediaItems); 171 mSuggestedMediaItems.clear(); 172 mSuggestedMediaItems.addAll(updatedSuggestedMediaItems); 173 mSpeakersAndDisplaysMediaItems.clear(); 174 mSpeakersAndDisplaysMediaItems.addAll(updatedSpeakersAndDisplaysMediaItems); 175 176 // The cached mOutputMediaItemList is cleared upon any update to individual media item 177 // lists. This ensures getOutputMediaItemList() computes and caches a fresh list on the next 178 // invocation. 179 mOutputMediaItemList.clear(); 180 } 181 182 /** Updates the list of output media items with the given list. */ clearAndAddAll(List<MediaItem> updatedMediaItems)183 public void clearAndAddAll(List<MediaItem> updatedMediaItems) { 184 mOutputMediaItemList.clear(); 185 mOutputMediaItemList.addAll(updatedMediaItems); 186 } 187 188 /** Removes the media items with muting expected devices. */ removeMutingExpectedDevices()189 public void removeMutingExpectedDevices() { 190 if (Flags.fixOutputMediaItemListIndexOutOfBoundsException()) { 191 mSelectedMediaItems.removeIf((MediaItem::isMutingExpectedDevice)); 192 mSuggestedMediaItems.removeIf((MediaItem::isMutingExpectedDevice)); 193 mSpeakersAndDisplaysMediaItems.removeIf((MediaItem::isMutingExpectedDevice)); 194 } 195 mOutputMediaItemList.removeIf((MediaItem::isMutingExpectedDevice)); 196 } 197 198 /** Clears the output media item list. */ clear()199 public void clear() { 200 if (Flags.fixOutputMediaItemListIndexOutOfBoundsException()) { 201 mSelectedMediaItems.clear(); 202 mSuggestedMediaItems.clear(); 203 mSpeakersAndDisplaysMediaItems.clear(); 204 } 205 mOutputMediaItemList.clear(); 206 } 207 208 /** Returns whether the output media item list is empty. */ isEmpty()209 public boolean isEmpty() { 210 if (Flags.fixOutputMediaItemListIndexOutOfBoundsException()) { 211 return mSelectedMediaItems.isEmpty() 212 && mSuggestedMediaItems.isEmpty() 213 && mSpeakersAndDisplaysMediaItems.isEmpty(); 214 } else { 215 return mOutputMediaItemList.isEmpty(); 216 } 217 } 218 buildMediaItems( List<MediaDevice> devices, Set<String> selectedOrConnectedMediaDeviceIds, boolean needToHandleMutingExpectedDevice, List<MediaItem> selectedMediaItems, List<MediaItem> suggestedMediaItems, List<MediaItem> speakersAndDisplaysMediaItems, Map<String, MediaItem> deviceIdToMediaItemMap)219 private void buildMediaItems( 220 List<MediaDevice> devices, 221 Set<String> selectedOrConnectedMediaDeviceIds, 222 boolean needToHandleMutingExpectedDevice, 223 List<MediaItem> selectedMediaItems, 224 List<MediaItem> suggestedMediaItems, 225 List<MediaItem> speakersAndDisplaysMediaItems, 226 Map<String, MediaItem> deviceIdToMediaItemMap) { 227 for (MediaDevice device : devices) { 228 String deviceId = device.getId(); 229 MediaItem mediaItem = MediaItem.createDeviceMediaItem(device); 230 if (needToHandleMutingExpectedDevice && device.isMutingExpectedDevice()) { 231 selectedMediaItems.add(0, mediaItem); 232 } else if (!needToHandleMutingExpectedDevice 233 && selectedOrConnectedMediaDeviceIds.contains(device.getId())) { 234 if (Flags.enableOutputSwitcherDeviceGrouping()) { 235 selectedMediaItems.add(mediaItem); 236 } else { 237 selectedMediaItems.add(0, mediaItem); 238 } 239 } else if (device.isSuggestedDevice()) { 240 suggestedMediaItems.add(mediaItem); 241 } else { 242 speakersAndDisplaysMediaItems.add(mediaItem); 243 } 244 deviceIdToMediaItemMap.put(deviceId, mediaItem); 245 } 246 } 247 248 /** Returns a list of media items that remains the same order as the existing media items. */ updateMediaItems( List<MediaItem> existingMediaItems, List<MediaItem> updatedMediaItems, Map<String, MediaItem> deviceIdToMediaItemMap, Set<String> updatedDeviceIds)249 private void updateMediaItems( 250 List<MediaItem> existingMediaItems, 251 List<MediaItem> updatedMediaItems, 252 Map<String, MediaItem> deviceIdToMediaItemMap, 253 Set<String> updatedDeviceIds) { 254 List<String> existingDeviceIds = getDeviceIds(existingMediaItems); 255 for (String deviceId : existingDeviceIds) { 256 MediaItem mediaItem = deviceIdToMediaItemMap.get(deviceId); 257 if (mediaItem != null) { 258 updatedMediaItems.add(mediaItem); 259 updatedDeviceIds.add(deviceId); 260 } 261 } 262 } 263 264 /** 265 * Returns media items from the input list that are not associated with the given device IDs. 266 */ getRemainingMediaItems( List<MediaItem> mediaItems, Set<String> deviceIds)267 private List<MediaItem> getRemainingMediaItems( 268 List<MediaItem> mediaItems, Set<String> deviceIds) { 269 List<MediaItem> remainingMediaItems = new ArrayList<>(); 270 for (MediaItem item : mediaItems) { 271 Optional<MediaDevice> mediaDeviceOptional = item.getMediaDevice(); 272 if (mediaDeviceOptional.isPresent()) { 273 String deviceId = mediaDeviceOptional.get().getId(); 274 if (!deviceIds.contains(deviceId)) { 275 remainingMediaItems.add(item); 276 } 277 } 278 } 279 return remainingMediaItems; 280 } 281 282 /** Returns a list of media device IDs for the given list of media items. */ getDeviceIds(List<MediaItem> mediaItems)283 private List<String> getDeviceIds(List<MediaItem> mediaItems) { 284 List<String> deviceIds = new ArrayList<>(); 285 for (MediaItem item : mediaItems) { 286 if (item != null && item.getMediaDevice().isPresent()) { 287 deviceIds.add(item.getMediaDevice().get().getId()); 288 } 289 } 290 return deviceIds; 291 } 292 } 293