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