• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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