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