1 /* 2 * Copyright (C) 2024 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.car.settings.sound; 18 19 import static android.car.media.CarAudioManager.AUDIO_FEATURE_DYNAMIC_ROUTING; 20 import static android.media.AudioDeviceInfo.TYPE_BLUETOOTH_A2DP; 21 22 import android.bluetooth.BluetoothProfile; 23 import android.car.media.AudioZoneConfigurationsChangeCallback; 24 import android.car.media.CarAudioManager; 25 import android.car.media.CarAudioZoneConfigInfo; 26 import android.car.media.CarVolumeGroupInfo; 27 import android.car.media.SwitchAudioZoneConfigCallback; 28 import android.content.Context; 29 import android.media.AudioAttributes; 30 import android.media.AudioDeviceAttributes; 31 import android.media.AudioDeviceInfo; 32 import android.util.ArrayMap; 33 import android.widget.Toast; 34 35 import androidx.annotation.NonNull; 36 import androidx.annotation.VisibleForTesting; 37 import androidx.core.content.ContextCompat; 38 39 import com.android.car.settings.CarSettingsApplication; 40 import com.android.car.settings.R; 41 import com.android.car.settings.common.Logger; 42 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 43 import com.android.settingslib.bluetooth.LocalBluetoothManager; 44 45 import java.util.ArrayList; 46 import java.util.List; 47 import java.util.Map; 48 49 /** 50 * Manages the audio routes. 51 */ 52 public class AudioRoutesManager { 53 private static final Logger LOG = new Logger(AudioRoutesManager.class); 54 private Context mContext; 55 private CarAudioManager mCarAudioManager; 56 private LocalBluetoothManager mBluetoothManager; 57 private int mAudioZone; 58 private int mUsage; 59 private String mActiveDeviceAddress; 60 private String mFutureActiveDeviceAddress; 61 private AudioZoneConfigUpdateListener mUpdateListener; 62 private List<String> mAddressList; 63 private Map<String, AudioRouteItem> mAudioRouteItemMap; 64 private Toast mToast; 65 66 /** 67 * A listener for when the AudioZoneConfig is updated. 68 */ 69 public interface AudioZoneConfigUpdateListener { onAudioZoneConfigUpdated()70 void onAudioZoneConfigUpdated(); 71 } 72 73 private final AudioZoneConfigurationsChangeCallback mAudioZoneConfigurationsChangeCallback = 74 new AudioZoneConfigurationsChangeCallback() { 75 @Override 76 public void onAudioZoneConfigurationsChanged( 77 @NonNull List<CarAudioZoneConfigInfo> configs, int status) { 78 AudioZoneConfigurationsChangeCallback.super.onAudioZoneConfigurationsChanged( 79 configs, status); 80 if (status == CarAudioManager.CONFIG_STATUS_CHANGED) { 81 setAudioRouteActive(); 82 } 83 } 84 }; 85 86 private final SwitchAudioZoneConfigCallback mSwitchAudioZoneConfigCallback = 87 (zoneConfig, isSuccessful) -> { 88 if (isSuccessful) { 89 mActiveDeviceAddress = mFutureActiveDeviceAddress; 90 if (mUpdateListener != null) { 91 mUpdateListener.onAudioZoneConfigUpdated(); 92 } 93 } else { 94 LOG.d("Switch audio zone failed."); 95 } 96 }; 97 AudioRoutesManager(Context context, int usage)98 public AudioRoutesManager(Context context, int usage) { 99 mContext = context; 100 mCarAudioManager = ((CarSettingsApplication) mContext.getApplicationContext()) 101 .getCarAudioManager(); 102 mAudioZone = ((CarSettingsApplication) mContext.getApplicationContext()) 103 .getMyAudioZoneId(); 104 mBluetoothManager = LocalBluetoothManager.getInstance(context, /* onInitCallback= */ null); 105 mUsage = usage; 106 mAudioRouteItemMap = new ArrayMap<>(); 107 mAddressList = new ArrayList<>(); 108 if (isAudioRoutingEnabled()) { 109 mCarAudioManager.clearAudioZoneConfigsCallback(); 110 mCarAudioManager.setAudioZoneConfigsChangeCallback( 111 ContextCompat.getMainExecutor(mContext), 112 mAudioZoneConfigurationsChangeCallback); 113 updateAudioRoutesList(); 114 } 115 } 116 updateAudioRoutesList()117 private void updateAudioRoutesList() { 118 List<CarAudioZoneConfigInfo> carAudioZoneConfigInfoList = 119 getCarAudioManager().getAudioZoneConfigInfos(mAudioZone); 120 for (CarAudioZoneConfigInfo carAudioZoneConfigInfo : carAudioZoneConfigInfoList) { 121 if (!carAudioZoneConfigInfo.isActive()) { 122 continue; 123 } 124 List<CarVolumeGroupInfo> carVolumeGroupInfoList = 125 carAudioZoneConfigInfo.getConfigVolumeGroups(); 126 for (CarVolumeGroupInfo carVolumeGroupInfo : carVolumeGroupInfoList) { 127 boolean isCorrectVolumeGroup = false; 128 for (AudioAttributes audioAttributes : carVolumeGroupInfo.getAudioAttributes()) { 129 if (audioAttributes.getUsage() == mUsage) { 130 isCorrectVolumeGroup = true; 131 break; 132 } 133 } 134 135 if (isCorrectVolumeGroup) { 136 List<AudioDeviceAttributes> audioDeviceAttributesList = 137 carVolumeGroupInfo.getAudioDeviceAttributes(); 138 for (AudioDeviceAttributes audioDeviceAttr : audioDeviceAttributesList) { 139 AudioRouteItem audioRouteItem = new AudioRouteItem(audioDeviceAttr); 140 mAddressList.add(audioRouteItem.getAddress()); 141 mAudioRouteItemMap.put(audioRouteItem.getAddress(), audioRouteItem); 142 } 143 } 144 } 145 } 146 147 List<CachedBluetoothDevice> bluetoothDevices = 148 mBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy().stream().toList(); 149 for (CachedBluetoothDevice bluetoothDevice : bluetoothDevices) { 150 if (bluetoothDevice.isConnectedA2dpDevice()) { 151 if (mAudioRouteItemMap.containsKey(bluetoothDevice.getAddress())) { 152 mAudioRouteItemMap.get(bluetoothDevice.getAddress()) 153 .setBluetoothDevice(bluetoothDevice); 154 mAudioRouteItemMap.get(bluetoothDevice.getAddress()) 155 .setAudioRouteType(TYPE_BLUETOOTH_A2DP); 156 } else { 157 AudioRouteItem audioRouteItem = new AudioRouteItem(bluetoothDevice); 158 mAddressList.add(audioRouteItem.getAddress()); 159 mAudioRouteItemMap.put(audioRouteItem.getAddress(), audioRouteItem); 160 } 161 } 162 } 163 164 AudioDeviceInfo deviceInfo = 165 mCarAudioManager.getOutputDeviceForUsage(mAudioZone, mUsage); 166 mActiveDeviceAddress = deviceInfo.getAddress(); 167 mFutureActiveDeviceAddress = mActiveDeviceAddress; 168 if (!mAudioRouteItemMap.containsKey(mActiveDeviceAddress)) { 169 LOG.d("The active device is not in the AudioDeviceAttributes list"); 170 } 171 } 172 173 /** 174 * Sets the {@link AudioZoneConfigUpdateListener}. 175 */ setUpdateListener(AudioZoneConfigUpdateListener listener)176 public void setUpdateListener(AudioZoneConfigUpdateListener listener) { 177 mUpdateListener = listener; 178 } 179 getAudioRouteList()180 public List<String> getAudioRouteList() { 181 return mAddressList; 182 } 183 getDeviceNameForAddress(String address)184 public String getDeviceNameForAddress(String address) { 185 if (mAudioRouteItemMap.containsKey(address)) { 186 return mAudioRouteItemMap.get(address).getName(); 187 } 188 return address; 189 } 190 191 @VisibleForTesting getAudioRouteItemMap()192 Map<String, AudioRouteItem> getAudioRouteItemMap() { 193 return mAudioRouteItemMap; 194 } 195 getActiveDeviceAddress()196 public String getActiveDeviceAddress() { 197 return mActiveDeviceAddress; 198 } 199 getCarAudioManager()200 public CarAudioManager getCarAudioManager() { 201 return mCarAudioManager; 202 } 203 isAudioRoutingEnabled()204 public boolean isAudioRoutingEnabled() { 205 if (mCarAudioManager != null 206 && getCarAudioManager().isAudioFeatureEnabled(AUDIO_FEATURE_DYNAMIC_ROUTING)) { 207 return true; 208 } 209 return false; 210 } 211 tearDown()212 public void tearDown() { 213 if (mCarAudioManager != null) { 214 mCarAudioManager.clearAudioZoneConfigsCallback(); 215 } 216 } 217 218 /** 219 * Update to a new audio destination of the provided address. 220 */ updateAudioRoute(String address)221 public AudioRouteItem updateAudioRoute(String address) { 222 showToast(address); 223 mFutureActiveDeviceAddress = address; 224 AudioRouteItem audioRouteItem = mAudioRouteItemMap.get(address); 225 if (audioRouteItem.getAudioRouteType() == TYPE_BLUETOOTH_A2DP) { 226 CachedBluetoothDevice bluetoothDevice = audioRouteItem.getBluetoothDevice(); 227 if (bluetoothDevice.isActiveDevice(BluetoothProfile.A2DP)) { 228 setAudioRouteActive(); 229 } else { 230 bluetoothDevice.setActive(); 231 } 232 } else { 233 setAudioRouteActive(); 234 } 235 return audioRouteItem; 236 } 237 setAudioRouteActive()238 private void setAudioRouteActive() { 239 List<CarAudioZoneConfigInfo> zoneConfigInfoList = 240 mCarAudioManager.getAudioZoneConfigInfos(mAudioZone); 241 for (CarAudioZoneConfigInfo carAudioZoneConfigInfo : zoneConfigInfoList) { 242 for (CarVolumeGroupInfo carVolumeGroupInfo : 243 carAudioZoneConfigInfo.getConfigVolumeGroups()) { 244 boolean hasCorrectUsage = false; 245 for (AudioAttributes audioAttributes : carVolumeGroupInfo.getAudioAttributes()) { 246 if (audioAttributes.getUsage() == mUsage) { 247 hasCorrectUsage = true; 248 break; 249 } 250 } 251 252 boolean hasCorrectAddress = false; 253 for (AudioDeviceAttributes audioDeviceAttributes : 254 carVolumeGroupInfo.getAudioDeviceAttributes()) { 255 if (mFutureActiveDeviceAddress.equals(audioDeviceAttributes.getAddress())) { 256 hasCorrectAddress = true; 257 break; 258 } 259 } 260 261 if (hasCorrectUsage && hasCorrectAddress) { 262 mCarAudioManager.switchAudioZoneToConfig(carAudioZoneConfigInfo, 263 ContextCompat.getMainExecutor(mContext), 264 mSwitchAudioZoneConfigCallback); 265 return; 266 } 267 } 268 } 269 } 270 showToast(String address)271 private void showToast(String address) { 272 if (mToast != null) { 273 mToast.cancel(); 274 } 275 String deviceName = getDeviceNameForAddress(address); 276 String text = mContext.getString(R.string.audio_route_selector_toast, deviceName); 277 int duration = mContext.getResources().getInteger(R.integer.audio_route_toast_duration); 278 mToast = Toast.makeText(mContext, text, duration); 279 mToast.show(); 280 } 281 } 282