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