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