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