• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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.settingslib.bluetooth;
18 
19 import static com.android.settingslib.bluetooth.HearingAidAudioRoutingConstants.BUILTIN_MIC;
20 import static com.android.settingslib.bluetooth.HearingAidAudioRoutingConstants.MICROPHONE_SOURCE_VOICE_COMMUNICATION;
21 
22 import android.content.Context;
23 import android.media.AudioAttributes;
24 import android.media.AudioDeviceAttributes;
25 import android.media.AudioDeviceInfo;
26 import android.media.AudioManager;
27 import android.media.audiopolicy.AudioProductStrategy;
28 import android.util.Log;
29 
30 import androidx.annotation.Nullable;
31 import androidx.annotation.VisibleForTesting;
32 
33 import com.android.settingslib.bluetooth.HearingAidAudioRoutingConstants.RoutingValue;
34 
35 import java.util.ArrayList;
36 import java.util.List;
37 import java.util.Set;
38 import java.util.stream.Collectors;
39 
40 /**
41  * A helper class to configure the audio routing for hearing aids.
42  */
43 public class HearingAidAudioRoutingHelper {
44 
45     private static final String TAG = "HearingAidAudioRoutingHelper";
46 
47     private final AudioManager mAudioManager;
48 
HearingAidAudioRoutingHelper(Context context)49     public HearingAidAudioRoutingHelper(Context context) {
50         mAudioManager = context.getSystemService(AudioManager.class);
51     }
52 
53     /**
54      * Gets the list of {@link AudioProductStrategy} referred by the given list of usage values
55      * defined in {@link AudioAttributes}
56      */
getSupportedStrategies(int[] attributeSdkUsageList)57     public List<AudioProductStrategy> getSupportedStrategies(int[] attributeSdkUsageList) {
58         final List<AudioAttributes> audioAttrList = new ArrayList<>(attributeSdkUsageList.length);
59         for (int attributeSdkUsage : attributeSdkUsageList) {
60             audioAttrList.add(new AudioAttributes.Builder().setUsage(attributeSdkUsage).build());
61         }
62 
63         final List<AudioProductStrategy> allStrategies = getAudioProductStrategies();
64         final List<AudioProductStrategy> supportedStrategies = new ArrayList<>();
65         for (AudioProductStrategy strategy : allStrategies) {
66             for (AudioAttributes audioAttr : audioAttrList) {
67                 if (strategy.supportsAudioAttributes(audioAttr)) {
68                     supportedStrategies.add(strategy);
69                 }
70             }
71         }
72 
73         return supportedStrategies.stream().distinct().collect(Collectors.toList());
74     }
75 
76     /**
77      * Sets the preferred device for the given strategies.
78      *
79      * @param supportedStrategies A list of {@link AudioProductStrategy} used to configure audio
80      *                            routing
81      * @param hearingDevice {@link AudioDeviceAttributes} of the device to be changed in audio
82      *                      routing
83      * @param routingValue one of value defined in
84      *                     {@link RoutingValue}, denotes routing
85      *                     destination.
86      * @return {code true} if the routing value successfully configure
87      */
setPreferredDeviceRoutingStrategies( List<AudioProductStrategy> supportedStrategies, AudioDeviceAttributes hearingDevice, @RoutingValue int routingValue)88     public boolean setPreferredDeviceRoutingStrategies(
89             List<AudioProductStrategy> supportedStrategies, AudioDeviceAttributes hearingDevice,
90             @RoutingValue int routingValue) {
91         boolean status;
92         switch (routingValue) {
93             case RoutingValue.AUTO:
94                 status = removePreferredDeviceForStrategies(supportedStrategies);
95                 return status;
96             case RoutingValue.HEARING_DEVICE:
97                 status = removePreferredDeviceForStrategies(supportedStrategies);
98                 status &= setPreferredDeviceForStrategies(supportedStrategies, hearingDevice);
99                 return status;
100             case RoutingValue.BUILTIN_DEVICE:
101                 status = removePreferredDeviceForStrategies(supportedStrategies);
102                 status &= setPreferredDeviceForStrategies(supportedStrategies,
103                         HearingAidAudioRoutingConstants.BUILTIN_SPEAKER);
104                 return status;
105             default:
106                 throw new IllegalArgumentException("Unexpected routingValue: " + routingValue);
107         }
108     }
109 
110     /**
111      * Set the preferred input device for calls.
112      *
113      * <p>Note that hearing device needs to be valid input device to be found in AudioManager.
114      * <p>Routing value can be:
115      * <ul>
116      *     <li> {@link RoutingValue#AUTO} - Allow the system to automatically select the appropriate
117      *     audio routing for calls.</li>
118      *     <li> {@link RoutingValue#HEARING_DEVICE} - Set input device to this hearing device.</li>
119      *     <li> {@link RoutingValue#BUILTIN_DEVICE} - Set input device to builtin microphone. </li>
120      * </ul>
121      * @param routingValue The desired routing value for calls
122      * @return {@code true} if the operation was successful
123      */
setPreferredInputDeviceForCalls(@ullable CachedBluetoothDevice hearingDevice, @RoutingValue int routingValue)124     public boolean setPreferredInputDeviceForCalls(@Nullable CachedBluetoothDevice hearingDevice,
125             @RoutingValue int routingValue) {
126         AudioDeviceAttributes hearingDeviceAttributes = getMatchedHearingDeviceAttributesInput(
127                 hearingDevice);
128         if (hearingDeviceAttributes == null) {
129             Log.w(TAG, "Can not find expected input AudioDeviceAttributes for hearing device: "
130                     + hearingDevice.getDevice().getAnonymizedAddress());
131             return false;
132         }
133 
134         final int audioSource = MICROPHONE_SOURCE_VOICE_COMMUNICATION;
135         return switch (routingValue) {
136             case RoutingValue.AUTO ->
137                     mAudioManager.clearPreferredDevicesForCapturePreset(audioSource);
138             case RoutingValue.HEARING_DEVICE -> {
139                 mAudioManager.clearPreferredDevicesForCapturePreset(audioSource);
140                 yield mAudioManager.setPreferredDeviceForCapturePreset(audioSource,
141                         hearingDeviceAttributes);
142             }
143             case RoutingValue.BUILTIN_DEVICE -> {
144                 mAudioManager.clearPreferredDevicesForCapturePreset(audioSource);
145                 yield mAudioManager.setPreferredDeviceForCapturePreset(audioSource, BUILTIN_MIC);
146             }
147             default -> throw new IllegalArgumentException(
148                     "Unexpected routingValue: " + routingValue);
149         };
150     }
151 
152     /**
153      * Clears the preferred input device for calls.
154      *
155      * {@code true} if the operation was successful
156      */
clearPreferredInputDeviceForCalls()157     public boolean clearPreferredInputDeviceForCalls() {
158         return mAudioManager.clearPreferredDevicesForCapturePreset(
159                 MICROPHONE_SOURCE_VOICE_COMMUNICATION);
160     }
161 
162     /**
163      * Gets the matched output hearing device {@link AudioDeviceAttributes} for {@code device}.
164      *
165      * <p>Will also try to match the {@link CachedBluetoothDevice#getSubDevice()} and
166      * {@link CachedBluetoothDevice#getMemberDevice()} of {@code device}
167      *
168      * @param device the {@link CachedBluetoothDevice} need to be hearing aid device
169      * @return the requested AudioDeviceAttributes or {@code null} if not match
170      */
171     @Nullable
getMatchedHearingDeviceAttributesForOutput( @ullable CachedBluetoothDevice device)172     public AudioDeviceAttributes getMatchedHearingDeviceAttributesForOutput(
173             @Nullable CachedBluetoothDevice device) {
174         if (device == null || !device.isHearingAidDevice()) {
175             return null;
176         }
177 
178         AudioDeviceInfo[] audioDevices = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
179         for (AudioDeviceInfo audioDevice : audioDevices) {
180             //TODO: b/370812132 - Need to update if TYPE_LEA_HEARING_AID is added
181             // ASHA for TYPE_HEARING_AID, HAP for TYPE_BLE_HEADSET
182             if (audioDevice.getType() == AudioDeviceInfo.TYPE_HEARING_AID
183                     || audioDevice.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET) {
184                 if (matchAddress(device, audioDevice)) {
185                     return new AudioDeviceAttributes(audioDevice);
186                 }
187             }
188         }
189         return null;
190     }
191 
192     /**
193      * Gets the matched input hearing device {@link AudioDeviceAttributes} for {@code device}.
194      *
195      * <p>Will also try to match the {@link CachedBluetoothDevice#getSubDevice()} and
196      * {@link CachedBluetoothDevice#getMemberDevice()} of {@code device}
197      *
198      * @param device the {@link CachedBluetoothDevice} need to be hearing aid device
199      * @return the requested AudioDeviceAttributes or {@code null} if not match
200      */
201     @Nullable
getMatchedHearingDeviceAttributesInput( @ullable CachedBluetoothDevice device)202     private AudioDeviceAttributes getMatchedHearingDeviceAttributesInput(
203             @Nullable CachedBluetoothDevice device) {
204         if (device == null || !device.isHearingAidDevice()) {
205             return null;
206         }
207 
208         AudioDeviceInfo[] audioDevices = mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
209         for (AudioDeviceInfo audioDevice : audioDevices) {
210             //TODO: b/370812132 - Need to update if TYPE_LEA_HEARING_AID is added
211             // HAP for TYPE_BLE_HEADSET
212             if (audioDevice.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET) {
213                 if (matchAddress(device, audioDevice)) {
214                     return new AudioDeviceAttributes(audioDevice);
215                 }
216             }
217         }
218         return null;
219     }
220 
matchAddress(CachedBluetoothDevice device, AudioDeviceInfo audioDevice)221     private boolean matchAddress(CachedBluetoothDevice device, AudioDeviceInfo audioDevice) {
222         final String audioDeviceAddress = audioDevice.getAddress();
223         final CachedBluetoothDevice subDevice = device.getSubDevice();
224         final Set<CachedBluetoothDevice> memberDevices = device.getMemberDevice();
225 
226         return device.getAddress().equals(audioDeviceAddress)
227                 || (subDevice != null && subDevice.getAddress().equals(audioDeviceAddress))
228                 || (!memberDevices.isEmpty() && memberDevices.stream().anyMatch(
229                     m -> m.getAddress().equals(audioDeviceAddress)));
230     }
231 
setPreferredDeviceForStrategies(List<AudioProductStrategy> strategies, AudioDeviceAttributes audioDevice)232     private boolean setPreferredDeviceForStrategies(List<AudioProductStrategy> strategies,
233             AudioDeviceAttributes audioDevice) {
234         boolean status = true;
235         for (AudioProductStrategy strategy : strategies) {
236             status &= mAudioManager.setPreferredDeviceForStrategy(strategy, audioDevice);
237         }
238 
239         return status;
240     }
241 
removePreferredDeviceForStrategies(List<AudioProductStrategy> strategies)242     private boolean removePreferredDeviceForStrategies(List<AudioProductStrategy> strategies) {
243         boolean status = true;
244         for (AudioProductStrategy strategy : strategies) {
245             if (mAudioManager.getPreferredDeviceForStrategy(strategy) != null) {
246                 status &= mAudioManager.removePreferredDeviceForStrategy(strategy);
247             }
248         }
249 
250         return status;
251     }
252 
253     @VisibleForTesting
getAudioProductStrategies()254     public List<AudioProductStrategy> getAudioProductStrategies() {
255         return AudioManager.getAudioProductStrategies();
256     }
257 }
258