1 /*
2  * Copyright (C) 2023 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.tv.media;
18 
19 import static com.android.settingslib.media.MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE;
20 import static com.android.settingslib.media.MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE;
21 import static com.android.settingslib.media.MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE;
22 import static com.android.settingslib.media.MediaDevice.MediaDeviceType.TYPE_CAST_GROUP_DEVICE;
23 import static com.android.settingslib.media.MediaDevice.MediaDeviceType.TYPE_FAST_PAIR_BLUETOOTH_DEVICE;
24 import static com.android.settingslib.media.MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE;
25 import static com.android.settingslib.media.MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE;
26 
27 import android.app.KeyguardManager;
28 import android.content.Context;
29 import android.media.AudioManager;
30 import android.media.session.MediaSessionManager;
31 import android.os.PowerExemptionManager;
32 import android.text.TextUtils;
33 
34 import com.android.settingslib.bluetooth.LocalBluetoothManager;
35 import com.android.settingslib.media.MediaDevice;
36 import com.android.systemui.animation.DialogTransitionAnimator;
37 import com.android.systemui.flags.FeatureFlags;
38 import com.android.systemui.media.dialog.MediaItem;
39 import com.android.systemui.media.dialog.MediaOutputController;
40 import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
41 import com.android.systemui.plugins.ActivityStarter;
42 import com.android.systemui.settings.UserTracker;
43 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
44 import com.android.systemui.tv.res.R;
45 
46 import org.jetbrains.annotations.NotNull;
47 
48 import java.util.ArrayList;
49 import java.util.List;
50 
51 /**
52  * Extends {@link MediaOutputController} to create a TV specific ordering and grouping of devices
53  * which are shown in the {@link TvMediaOutputDialogActivity}.
54  */
55 public class TvMediaOutputController extends MediaOutputController {
56 
57     private final Context mContext;
58     private final AudioManager mAudioManager;
59 
TvMediaOutputController( @otNull Context context, String packageName, MediaSessionManager mediaSessionManager, LocalBluetoothManager lbm, ActivityStarter starter, CommonNotifCollection notifCollection, DialogTransitionAnimator dialogTransitionAnimator, NearbyMediaDevicesManager nearbyMediaDevicesManager, AudioManager audioManager, PowerExemptionManager powerExemptionManager, KeyguardManager keyGuardManager, FeatureFlags featureFlags, UserTracker userTracker)60     public TvMediaOutputController(
61             @NotNull Context context,
62             String packageName,
63             MediaSessionManager mediaSessionManager,
64             LocalBluetoothManager lbm,
65             ActivityStarter starter,
66             CommonNotifCollection notifCollection,
67             DialogTransitionAnimator dialogTransitionAnimator,
68             NearbyMediaDevicesManager nearbyMediaDevicesManager,
69             AudioManager audioManager,
70             PowerExemptionManager powerExemptionManager,
71             KeyguardManager keyGuardManager,
72             FeatureFlags featureFlags,
73             UserTracker userTracker) {
74         super(
75                 context,
76                 packageName,
77                 /* userHandle= */ null,
78                 /* token= */ null,
79                 mediaSessionManager,
80                 lbm,
81                 starter,
82                 notifCollection,
83                 dialogTransitionAnimator,
84                 nearbyMediaDevicesManager,
85                 audioManager,
86                 powerExemptionManager,
87                 keyGuardManager,
88                 featureFlags,
89                 userTracker);
90         mContext = context;
91         mAudioManager = audioManager;
92     }
93 
showVolumeDialog()94     void showVolumeDialog() {
95         mAudioManager.adjustVolume(AudioManager.ADJUST_SAME, AudioManager.FLAG_SHOW_UI);
96     }
97 
98     /**
99      * Assigns lower priorities to devices that should be shown higher up in the list.
100      */
getDevicePriorityGroup(MediaDevice mediaDevice)101     private int getDevicePriorityGroup(MediaDevice mediaDevice) {
102         int mediaDeviceType = mediaDevice.getDeviceType();
103         return switch (mediaDeviceType) {
104             case TYPE_PHONE_DEVICE -> 1;
105             case TYPE_USB_C_AUDIO_DEVICE -> 2;
106             case TYPE_3POINT5_MM_AUDIO_DEVICE -> 3;
107             case TYPE_CAST_DEVICE, TYPE_CAST_GROUP_DEVICE, TYPE_BLUETOOTH_DEVICE,
108                     TYPE_FAST_PAIR_BLUETOOTH_DEVICE -> 5;
109             default -> 4;
110         };
111     }
112 
sortMediaDevices(List<MediaDevice> mediaDevices)113     private void sortMediaDevices(List<MediaDevice> mediaDevices) {
114         mediaDevices.sort((device1, device2) -> {
115             int priority1 = getDevicePriorityGroup(device1);
116             int priority2 = getDevicePriorityGroup(device2);
117 
118             if (priority1 != priority2) {
119                 return (priority1 < priority2) ? -1 : 1;
120             }
121             // Show connected before disconnected devices
122             if (device1.isConnected() != device2.isConnected()) {
123                 return device1.isConnected() ? -1 : 1;
124             }
125             return device1.getName().compareToIgnoreCase(device2.getName());
126         });
127     }
128 
129     @Override
buildMediaItems(List<MediaItem> oldMediaItems, List<MediaDevice> devices)130     protected List<MediaItem> buildMediaItems(List<MediaItem> oldMediaItems,
131             List<MediaDevice> devices) {
132         synchronized (mMediaDevicesLock) {
133             if (oldMediaItems.isEmpty()) {
134                 return buildInitialList(devices);
135             }
136             return buildBetterSubsequentList(oldMediaItems, devices);
137         }
138     }
139 
buildInitialList(List<MediaDevice> devices)140     private List<MediaItem> buildInitialList(List<MediaDevice> devices) {
141         sortMediaDevices(devices);
142 
143         List<MediaItem> finalMediaItems = new ArrayList<>();
144         boolean disconnectedDevicesAdded = false;
145         for (MediaDevice device : devices) {
146             // Add divider before first disconnected device
147             if (!device.isConnected() && !disconnectedDevicesAdded) {
148                 addOtherDevicesDivider(finalMediaItems);
149                 disconnectedDevicesAdded = true;
150             }
151             finalMediaItems.add(MediaItem.createDeviceMediaItem(device));
152         }
153         addConnectAnotherDeviceItem(finalMediaItems);
154         return finalMediaItems;
155     }
156 
157     /**
158      * Keep devices that have not changed their connection state in the same order.
159      * If there is a new connected device, put it at the *bottom* of the connected devices list and
160      * if there is a newly disconnected device, add it at the *top* of the disconnected devices.
161      */
buildBetterSubsequentList(List<MediaItem> previousMediaItems, List<MediaDevice> devices)162     private List<MediaItem> buildBetterSubsequentList(List<MediaItem> previousMediaItems,
163             List<MediaDevice> devices) {
164 
165         final List<MediaItem> targetMediaItems = new ArrayList<>();
166         // Only use the actual devices, not the dividers etc.
167         List<MediaItem> oldMediaItems = previousMediaItems.stream()
168                 .filter(mediaItem -> mediaItem.getMediaDevice().isPresent()).toList();
169         addItemsBasedOnConnection(targetMediaItems, oldMediaItems, devices,
170                 /* isConnected= */ true);
171         addItemsBasedOnConnection(targetMediaItems, oldMediaItems, devices,
172                 /* isConnected= */ false);
173 
174         addConnectAnotherDeviceItem(targetMediaItems);
175         return targetMediaItems;
176     }
177 
addItemsBasedOnConnection(List<MediaItem> targetMediaItems, List<MediaItem> oldMediaItems, List<MediaDevice> devices, boolean isConnected)178     private void addItemsBasedOnConnection(List<MediaItem> targetMediaItems,
179             List<MediaItem> oldMediaItems, List<MediaDevice> devices, boolean isConnected) {
180 
181         List<MediaDevice> matchingMediaDevices = new ArrayList<>();
182         for (MediaItem originalMediaItem : oldMediaItems) {
183             // Only go through the device items
184             MediaDevice oldDevice = originalMediaItem.getMediaDevice().get();
185 
186             for (MediaDevice newDevice : devices) {
187                 if (TextUtils.equals(oldDevice.getId(), newDevice.getId())
188                         && oldDevice.isConnected() == isConnected
189                         && newDevice.isConnected() == isConnected) {
190                     matchingMediaDevices.add(newDevice);
191                     break;
192                 }
193             }
194         }
195         devices.removeAll(matchingMediaDevices);
196 
197         List<MediaDevice> newMediaDevices = new ArrayList<>();
198         for (MediaDevice remainingDevice : devices) {
199             if (remainingDevice.isConnected() == isConnected) {
200                 newMediaDevices.add(remainingDevice);
201             }
202         }
203         devices.removeAll(newMediaDevices);
204 
205         // Add new connected devices at the end, add new disconnected devices at the start
206         if (isConnected) {
207             targetMediaItems.addAll(
208                     matchingMediaDevices.stream().map(MediaItem::createDeviceMediaItem).toList());
209             targetMediaItems.addAll(
210                     newMediaDevices.stream().map(MediaItem::createDeviceMediaItem).toList());
211         } else {
212             if (!matchingMediaDevices.isEmpty() || !newMediaDevices.isEmpty()) {
213                 addOtherDevicesDivider(targetMediaItems);
214             }
215             targetMediaItems.addAll(
216                     newMediaDevices.stream().map(MediaItem::createDeviceMediaItem).toList());
217             targetMediaItems.addAll(
218                     matchingMediaDevices.stream().map(MediaItem::createDeviceMediaItem).toList());
219         }
220     }
221 
addOtherDevicesDivider(List<MediaItem> mediaItems)222     private void addOtherDevicesDivider(List<MediaItem> mediaItems) {
223         mediaItems.add(
224                 MediaItem.createGroupDividerMediaItem(
225                         mContext.getString(R.string.media_output_dialog_other_devices)));
226     }
227 
addConnectAnotherDeviceItem(List<MediaItem> mediaItems)228     private void addConnectAnotherDeviceItem(List<MediaItem> mediaItems) {
229         mediaItems.add(MediaItem.createGroupDividerMediaItem(/* title */ null));
230         mediaItems.add(MediaItem.createPairNewDeviceMediaItem());
231     }
232 
233     @Override
start(@otNull Callback cb)234     protected void start(@NotNull Callback cb) {
235         super.start(cb);
236     }
237 
238     @Override
stop()239     protected void stop() {
240         super.stop();
241     }
242 
243     @Override
setTemporaryAllowListExceptionIfNeeded(MediaDevice targetDevice)244     protected void setTemporaryAllowListExceptionIfNeeded(MediaDevice targetDevice) {
245         super.setTemporaryAllowListExceptionIfNeeded(targetDevice);
246     }
247 
248     @Override
connectDevice(MediaDevice mediaDevice)249     protected void connectDevice(MediaDevice mediaDevice) {
250         super.connectDevice(mediaDevice);
251     }
252 }
253