• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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