/* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.car.settings.sound; import static android.car.media.CarAudioManager.AUDIO_FEATURE_DYNAMIC_ROUTING; import static android.media.AudioDeviceInfo.TYPE_BLUETOOTH_A2DP; import android.bluetooth.BluetoothProfile; import android.car.media.AudioZoneConfigurationsChangeCallback; import android.car.media.CarAudioManager; import android.car.media.CarAudioZoneConfigInfo; import android.car.media.CarVolumeGroupInfo; import android.car.media.SwitchAudioZoneConfigCallback; import android.content.Context; import android.media.AudioAttributes; import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; import android.util.ArrayMap; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.core.content.ContextCompat; import com.android.car.settings.CarSettingsApplication; import com.android.car.settings.R; import com.android.car.settings.common.Logger; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.LocalBluetoothManager; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * Manages the audio routes. */ public class AudioRoutesManager { private static final Logger LOG = new Logger(AudioRoutesManager.class); private Context mContext; private CarAudioManager mCarAudioManager; private LocalBluetoothManager mBluetoothManager; private int mAudioZone; private int mUsage; private String mActiveDeviceAddress; private String mFutureActiveDeviceAddress; private AudioZoneConfigUpdateListener mUpdateListener; private List mAddressList; private Map mAudioRouteItemMap; private Toast mToast; /** * A listener for when the AudioZoneConfig is updated. */ public interface AudioZoneConfigUpdateListener { void onAudioZoneConfigUpdated(); } private final AudioZoneConfigurationsChangeCallback mAudioZoneConfigurationsChangeCallback = new AudioZoneConfigurationsChangeCallback() { @Override public void onAudioZoneConfigurationsChanged( @NonNull List configs, int status) { AudioZoneConfigurationsChangeCallback.super.onAudioZoneConfigurationsChanged( configs, status); if (status == CarAudioManager.CONFIG_STATUS_CHANGED) { setAudioRouteActive(); } } }; private final SwitchAudioZoneConfigCallback mSwitchAudioZoneConfigCallback = (zoneConfig, isSuccessful) -> { if (isSuccessful) { mActiveDeviceAddress = mFutureActiveDeviceAddress; if (mUpdateListener != null) { mUpdateListener.onAudioZoneConfigUpdated(); } } else { LOG.d("Switch audio zone failed."); } }; public AudioRoutesManager(Context context, int usage) { mContext = context; mCarAudioManager = ((CarSettingsApplication) mContext.getApplicationContext()) .getCarAudioManager(); mAudioZone = ((CarSettingsApplication) mContext.getApplicationContext()) .getMyAudioZoneId(); mBluetoothManager = LocalBluetoothManager.getInstance(context, /* onInitCallback= */ null); mUsage = usage; mAudioRouteItemMap = new ArrayMap<>(); mAddressList = new ArrayList<>(); if (isAudioRoutingEnabled()) { mCarAudioManager.clearAudioZoneConfigsCallback(); mCarAudioManager.setAudioZoneConfigsChangeCallback( ContextCompat.getMainExecutor(mContext), mAudioZoneConfigurationsChangeCallback); updateAudioRoutesList(); } } private void updateAudioRoutesList() { List carAudioZoneConfigInfoList = getCarAudioManager().getAudioZoneConfigInfos(mAudioZone); for (CarAudioZoneConfigInfo carAudioZoneConfigInfo : carAudioZoneConfigInfoList) { if (!carAudioZoneConfigInfo.isActive()) { continue; } List carVolumeGroupInfoList = carAudioZoneConfigInfo.getConfigVolumeGroups(); for (CarVolumeGroupInfo carVolumeGroupInfo : carVolumeGroupInfoList) { boolean isCorrectVolumeGroup = false; for (AudioAttributes audioAttributes : carVolumeGroupInfo.getAudioAttributes()) { if (audioAttributes.getUsage() == mUsage) { isCorrectVolumeGroup = true; break; } } if (isCorrectVolumeGroup) { List audioDeviceAttributesList = carVolumeGroupInfo.getAudioDeviceAttributes(); for (AudioDeviceAttributes audioDeviceAttr : audioDeviceAttributesList) { AudioRouteItem audioRouteItem = new AudioRouteItem(audioDeviceAttr); mAddressList.add(audioRouteItem.getAddress()); mAudioRouteItemMap.put(audioRouteItem.getAddress(), audioRouteItem); } } } } List bluetoothDevices = mBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy().stream().toList(); for (CachedBluetoothDevice bluetoothDevice : bluetoothDevices) { if (bluetoothDevice.isConnectedA2dpDevice()) { if (mAudioRouteItemMap.containsKey(bluetoothDevice.getAddress())) { mAudioRouteItemMap.get(bluetoothDevice.getAddress()) .setBluetoothDevice(bluetoothDevice); mAudioRouteItemMap.get(bluetoothDevice.getAddress()) .setAudioRouteType(TYPE_BLUETOOTH_A2DP); } else { AudioRouteItem audioRouteItem = new AudioRouteItem(bluetoothDevice); mAddressList.add(audioRouteItem.getAddress()); mAudioRouteItemMap.put(audioRouteItem.getAddress(), audioRouteItem); } } } AudioDeviceInfo deviceInfo = mCarAudioManager.getOutputDeviceForUsage(mAudioZone, mUsage); mActiveDeviceAddress = deviceInfo.getAddress(); mFutureActiveDeviceAddress = mActiveDeviceAddress; if (!mAudioRouteItemMap.containsKey(mActiveDeviceAddress)) { LOG.d("The active device is not in the AudioDeviceAttributes list"); } } /** * Sets the {@link AudioZoneConfigUpdateListener}. */ public void setUpdateListener(AudioZoneConfigUpdateListener listener) { mUpdateListener = listener; } public List getAudioRouteList() { return mAddressList; } public String getDeviceNameForAddress(String address) { if (mAudioRouteItemMap.containsKey(address)) { return mAudioRouteItemMap.get(address).getName(); } return address; } @VisibleForTesting Map getAudioRouteItemMap() { return mAudioRouteItemMap; } public String getActiveDeviceAddress() { return mActiveDeviceAddress; } public CarAudioManager getCarAudioManager() { return mCarAudioManager; } public boolean isAudioRoutingEnabled() { if (mCarAudioManager != null && getCarAudioManager().isAudioFeatureEnabled(AUDIO_FEATURE_DYNAMIC_ROUTING)) { return true; } return false; } public void tearDown() { if (mCarAudioManager != null) { mCarAudioManager.clearAudioZoneConfigsCallback(); } } /** * Update to a new audio destination of the provided address. */ public AudioRouteItem updateAudioRoute(String address) { showToast(address); mFutureActiveDeviceAddress = address; AudioRouteItem audioRouteItem = mAudioRouteItemMap.get(address); if (audioRouteItem.getAudioRouteType() == TYPE_BLUETOOTH_A2DP) { CachedBluetoothDevice bluetoothDevice = audioRouteItem.getBluetoothDevice(); if (bluetoothDevice.isActiveDevice(BluetoothProfile.A2DP)) { setAudioRouteActive(); } else { bluetoothDevice.setActive(); } } else { setAudioRouteActive(); } return audioRouteItem; } private void setAudioRouteActive() { List zoneConfigInfoList = mCarAudioManager.getAudioZoneConfigInfos(mAudioZone); for (CarAudioZoneConfigInfo carAudioZoneConfigInfo : zoneConfigInfoList) { for (CarVolumeGroupInfo carVolumeGroupInfo : carAudioZoneConfigInfo.getConfigVolumeGroups()) { boolean hasCorrectUsage = false; for (AudioAttributes audioAttributes : carVolumeGroupInfo.getAudioAttributes()) { if (audioAttributes.getUsage() == mUsage) { hasCorrectUsage = true; break; } } boolean hasCorrectAddress = false; for (AudioDeviceAttributes audioDeviceAttributes : carVolumeGroupInfo.getAudioDeviceAttributes()) { if (mFutureActiveDeviceAddress.equals(audioDeviceAttributes.getAddress())) { hasCorrectAddress = true; break; } } if (hasCorrectUsage && hasCorrectAddress) { mCarAudioManager.switchAudioZoneToConfig(carAudioZoneConfigInfo, ContextCompat.getMainExecutor(mContext), mSwitchAudioZoneConfigCallback); return; } } } } private void showToast(String address) { if (mToast != null) { mToast.cancel(); } String deviceName = getDeviceNameForAddress(address); String text = mContext.getString(R.string.audio_route_selector_toast, deviceName); int duration = mContext.getResources().getInteger(R.integer.audio_route_toast_duration); mToast = Toast.makeText(mContext, text, duration); mToast.show(); } }