1 /* 2 * Copyright (C) 2021 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 android.car.builtin.media; 18 19 import static android.media.AudioAttributes.USAGE_VIRTUAL_SOURCE; 20 import static android.media.AudioManager.EXTRA_VOLUME_STREAM_TYPE; 21 import static android.media.AudioManager.GET_DEVICES_INPUTS; 22 import static android.media.AudioManager.GET_DEVICES_OUTPUTS; 23 import static android.media.AudioManager.MASTER_MUTE_CHANGED_ACTION; 24 import static android.media.AudioManager.VOLUME_CHANGED_ACTION; 25 26 import android.annotation.NonNull; 27 import android.annotation.SystemApi; 28 import android.car.builtin.annotation.AddedIn; 29 import android.car.builtin.annotation.PlatformVersion; 30 import android.car.builtin.util.Slogf; 31 import android.content.BroadcastReceiver; 32 import android.content.Context; 33 import android.content.Intent; 34 import android.content.IntentFilter; 35 import android.media.AudioAttributes; 36 import android.media.AudioAttributes.AttributeUsage; 37 import android.media.AudioDeviceInfo; 38 import android.media.AudioDevicePort; 39 import android.media.AudioFormat; 40 import android.media.AudioGain; 41 import android.media.AudioGainConfig; 42 import android.media.AudioManager; 43 import android.media.AudioPatch; 44 import android.media.AudioPortConfig; 45 import android.media.AudioSystem; 46 47 import com.android.internal.util.Preconditions; 48 49 import java.util.ArrayList; 50 import java.util.Objects; 51 52 /** 53 * Helper for Audio related operations. 54 * 55 * @hide 56 */ 57 @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) 58 public final class AudioManagerHelper { 59 60 @AddedIn(PlatformVersion.TIRAMISU_0) 61 public static final int UNDEFINED_STREAM_TYPE = -1; 62 private static final String TAG = "AudioServiceHelper"; 63 AudioManagerHelper()64 private AudioManagerHelper() { 65 throw new UnsupportedOperationException(); 66 } 67 68 /** 69 * Set the audio device gain for device with {@code address} 70 * @param audioManager audio manager 71 * @param address Address for device to set gain 72 * @param gainInMillibels gain in millibels to set 73 * @param isOutput is the device an output device 74 * @return true if the gain was successfully set 75 */ 76 @AddedIn(PlatformVersion.TIRAMISU_0) setAudioDeviceGain(@onNull AudioManager audioManager, @NonNull String address, int gainInMillibels, boolean isOutput)77 public static boolean setAudioDeviceGain(@NonNull AudioManager audioManager, 78 @NonNull String address, int gainInMillibels, boolean isOutput) { 79 Preconditions.checkNotNull(audioManager, 80 "Audio Manager can not be null in set device gain, device address %s", address); 81 AudioDeviceInfo deviceInfo = getAudioDeviceInfo(audioManager, address, isOutput); 82 83 AudioGain audioGain = getAudioGain(deviceInfo.getPort()); 84 85 // size of gain values is 1 in MODE_JOINT 86 AudioGainConfig audioGainConfig = audioGain.buildConfig( 87 AudioGain.MODE_JOINT, 88 audioGain.channelMask(), 89 new int[] { gainInMillibels }, 90 0); 91 if (audioGainConfig == null) { 92 throw new IllegalStateException("Failed to construct AudioGainConfig for device " 93 + address); 94 } 95 96 int r = AudioManager.setAudioPortGain(deviceInfo.getPort(), audioGainConfig); 97 if (r == AudioManager.SUCCESS) { 98 return true; 99 } 100 return false; 101 } 102 getAudioDeviceInfo(@onNull AudioManager audioManager, @NonNull String address, boolean isOutput)103 private static AudioDeviceInfo getAudioDeviceInfo(@NonNull AudioManager audioManager, 104 @NonNull String address, boolean isOutput) { 105 Objects.requireNonNull(address, "Device address can not be null"); 106 Preconditions.checkStringNotEmpty(address, "Device Address can not be empty"); 107 108 AudioDeviceInfo[] devices = 109 audioManager.getDevices(isOutput ? GET_DEVICES_OUTPUTS : GET_DEVICES_INPUTS); 110 111 for (int index = 0; index < devices.length; index++) { 112 AudioDeviceInfo device = devices[index]; 113 if (address.equals(device.getAddress())) { 114 return device; 115 } 116 } 117 118 throw new IllegalStateException((isOutput ? "Output" : "Input") 119 + " Audio device info not found for device address " + address); 120 } 121 getAudioGain(@onNull AudioDevicePort deviceport)122 private static AudioGain getAudioGain(@NonNull AudioDevicePort deviceport) { 123 Objects.requireNonNull(deviceport, "Audio device port can not be null"); 124 Preconditions.checkArgument(deviceport.gains().length > 0, 125 "Audio device must have gains defined"); 126 for (int index = 0; index < deviceport.gains().length; index++) { 127 AudioGain gain = deviceport.gains()[index]; 128 if ((gain.mode() & AudioGain.MODE_JOINT) != 0) { 129 return checkAudioGainConfiguration(gain); 130 } 131 } 132 throw new IllegalStateException("Audio device does not have a valid audio gain"); 133 } 134 checkAudioGainConfiguration(@onNull AudioGain audioGain)135 private static AudioGain checkAudioGainConfiguration(@NonNull AudioGain audioGain) { 136 Preconditions.checkArgument(audioGain.maxValue() >= audioGain.minValue(), 137 "Max gain %d is lower than min gain %d", 138 audioGain.maxValue(), audioGain.minValue()); 139 Preconditions.checkArgument((audioGain.defaultValue() >= audioGain.minValue()) 140 && (audioGain.defaultValue() <= audioGain.maxValue()), 141 "Default gain %d not in range (%d,%d)", audioGain.defaultValue(), 142 audioGain.minValue(), audioGain.maxValue()); 143 Preconditions.checkArgument( 144 ((audioGain.maxValue() - audioGain.minValue()) % audioGain.stepValue()) == 0, 145 "Gain step value %d greater than min gain to max gain range %d", 146 audioGain.stepValue(), audioGain.maxValue() - audioGain.minValue()); 147 Preconditions.checkArgument( 148 ((audioGain.defaultValue() - audioGain.minValue()) % audioGain.stepValue()) == 0, 149 "Gain step value %d greater than min gain to default gain range %d", 150 audioGain.stepValue(), audioGain.defaultValue() - audioGain.minValue()); 151 return audioGain; 152 } 153 154 /** 155 * Returns the audio gain information for the specified device. 156 * @param deviceInfo 157 * @return 158 */ 159 @AddedIn(PlatformVersion.TIRAMISU_0) getAudioGainInfo(@onNull AudioDeviceInfo deviceInfo)160 public static AudioGainInfo getAudioGainInfo(@NonNull AudioDeviceInfo deviceInfo) { 161 Objects.requireNonNull(deviceInfo); 162 return new AudioGainInfo(getAudioGain(deviceInfo.getPort())); 163 } 164 165 /** 166 * Creates an audio patch from source and sink source 167 * @param sourceDevice Source device for the patch 168 * @param sinkDevice Sink device of the patch 169 * @param gainInMillibels gain to apply to the source device 170 * @return The audio patch information that was created 171 */ 172 @AddedIn(PlatformVersion.TIRAMISU_0) createAudioPatch(@onNull AudioDeviceInfo sourceDevice, @NonNull AudioDeviceInfo sinkDevice, int gainInMillibels)173 public static AudioPatchInfo createAudioPatch(@NonNull AudioDeviceInfo sourceDevice, 174 @NonNull AudioDeviceInfo sinkDevice, int gainInMillibels) { 175 Preconditions.checkNotNull(sourceDevice, 176 "Source device can not be null, sink info %s", sinkDevice); 177 Preconditions.checkNotNull(sinkDevice, 178 "Sink device can not be null, source info %s", sourceDevice); 179 180 AudioDevicePort sinkPort = Preconditions.checkNotNull(sinkDevice.getPort(), 181 "Sink device [%s] does not contain an audio port", sinkDevice); 182 183 // {@link android.media.AudioPort#activeConfig()} is valid for mixer port only, 184 // since audio framework has no clue what's active on the device ports. 185 // Therefore we construct an empty / default configuration here, which the audio HAL 186 // implementation should ignore. 187 AudioPortConfig sinkConfig = sinkPort.buildConfig(0, 188 AudioFormat.CHANNEL_OUT_DEFAULT, AudioFormat.ENCODING_DEFAULT, null); 189 Slogf.d(TAG, "createAudioPatch sinkConfig: " + sinkConfig); 190 191 // Configure the source port to match the output port except for a gain adjustment 192 AudioGain audioGain = Objects.requireNonNull(getAudioGain(sourceDevice.getPort()), 193 "Gain controller not available for source port"); 194 195 // size of gain values is 1 in MODE_JOINT 196 AudioGainConfig audioGainConfig = audioGain.buildConfig(AudioGain.MODE_JOINT, 197 audioGain.channelMask(), new int[] { gainInMillibels }, 0); 198 // Construct an empty / default configuration excepts gain config here and it's up to the 199 // audio HAL how to interpret this configuration, which the audio HAL 200 // implementation should ignore. 201 AudioPortConfig sourceConfig = sourceDevice.getPort().buildConfig(0, 202 AudioFormat.CHANNEL_IN_DEFAULT, AudioFormat.ENCODING_DEFAULT, audioGainConfig); 203 204 // Create an audioPatch to connect the two ports 205 AudioPatch[] patch = new AudioPatch[] { null }; 206 int result = AudioManager.createAudioPatch(patch, 207 new AudioPortConfig[] { sourceConfig }, 208 new AudioPortConfig[] { sinkConfig }); 209 if (result != AudioManager.SUCCESS) { 210 throw new RuntimeException("createAudioPatch failed with code " + result); 211 } 212 213 Preconditions.checkNotNull(patch[0], 214 "createAudioPatch didn't provide expected single handle [source: %s,sink: %s]", 215 sinkDevice, sourceDevice); 216 Slogf.d(TAG, "Audio patch created: " + patch[0]); 217 218 return createAudioPatchInfo(patch[0]); 219 } 220 createAudioPatchInfo(AudioPatch patch)221 private static AudioPatchInfo createAudioPatchInfo(AudioPatch patch) { 222 Preconditions.checkArgument(patch.sources().length == 1 223 && patch.sources()[0].port() instanceof AudioDevicePort, 224 "Accepts exactly one device port as source"); 225 Preconditions.checkArgument(patch.sinks().length == 1 226 && patch.sinks()[0].port() instanceof AudioDevicePort, 227 "Accepts exactly one device port as sink"); 228 229 return new AudioPatchInfo(((AudioDevicePort) patch.sources()[0].port()).address(), 230 ((AudioDevicePort) patch.sinks()[0].port()).address(), 231 patch.id()); 232 } 233 234 /** 235 * Releases audio patch handle 236 * @param audioManager manager to call for releasing of handle 237 * @param info patch information to release 238 * @return returns true if the patch was successfully removed 239 */ 240 @AddedIn(PlatformVersion.TIRAMISU_0) releaseAudioPatch(@onNull AudioManager audioManager, @NonNull AudioPatchInfo info)241 public static boolean releaseAudioPatch(@NonNull AudioManager audioManager, 242 @NonNull AudioPatchInfo info) { 243 Preconditions.checkNotNull(audioManager, 244 "Audio Manager can not be null in release audio patch for %s", info); 245 Preconditions.checkNotNull(info, 246 "Audio Patch Info can not be null in release audio patch for %s", info); 247 // NOTE: AudioPolicyService::removeNotificationClient will take care of this automatically 248 // if the client that created a patch quits. 249 ArrayList<AudioPatch> patches = new ArrayList<>(); 250 int result = audioManager.listAudioPatches(patches); 251 if (result != AudioManager.SUCCESS) { 252 throw new RuntimeException("listAudioPatches failed with code " + result); 253 } 254 255 // Look for a patch that matches the provided user side handle 256 for (AudioPatch patch : patches) { 257 if (info.represents(patch)) { 258 // Found it! 259 result = AudioManager.releaseAudioPatch(patch); 260 if (result != AudioManager.SUCCESS) { 261 throw new RuntimeException("releaseAudioPatch failed with code " + result); 262 } 263 return true; 264 } 265 } 266 return false; 267 } 268 269 /** 270 * Returns the string representation of {@link android.media.AudioAttributes.AttributeUsage}. 271 * 272 * <p>See {@link android.media.AudioAttributes.usageToString}. 273 */ 274 @AddedIn(PlatformVersion.TIRAMISU_0) usageToString(@ttributeUsage int usage)275 public static String usageToString(@AttributeUsage int usage) { 276 return AudioAttributes.usageToString(usage); 277 } 278 279 /** 280 * Returns the xsd string representation of 281 * {@link android.media.AudioAttributes.AttributeUsage}. 282 * 283 * <p>See {@link android.media.AudioAttributes.usageToXsdString}. 284 */ 285 @AddedIn(PlatformVersion.TIRAMISU_0) usageToXsdString(@ttributeUsage int usage)286 public static String usageToXsdString(@AttributeUsage int usage) { 287 return AudioAttributes.usageToXsdString(usage); 288 } 289 290 /** 291 * Returns {@link android.media.AudioAttributes.AttributeUsage} representation of 292 * xsd usage string. 293 * 294 * <p>See {@link android.media.AudioAttributes.xsdStringToUsage}. 295 */ 296 @AddedIn(PlatformVersion.TIRAMISU_0) xsdStringToUsage(String usage)297 public static int xsdStringToUsage(String usage) { 298 return AudioAttributes.xsdStringToUsage(usage); 299 } 300 301 /** 302 * Returns {@link android.media.AudioAttributes.AttributeUsage} for 303 * {@link android.media.AudioAttributes.AttributeUsage.USAGE_VIRTUAL_SOURCE}. 304 */ 305 @AddedIn(PlatformVersion.TIRAMISU_0) getUsageVirtualSource()306 public static int getUsageVirtualSource() { 307 return USAGE_VIRTUAL_SOURCE; 308 } 309 310 /** 311 * Returns the string representation of volume adjustment. 312 * 313 * <p>See {@link android.media.AudioManager#adjustToString(int)} 314 */ 315 @AddedIn(PlatformVersion.TIRAMISU_0) adjustToString(int adjustment)316 public static String adjustToString(int adjustment) { 317 return AudioManager.adjustToString(adjustment); 318 } 319 320 /** 321 * Sets the system master mute state. 322 * 323 * <p>See {@link android.media.AudioManager#setMasterMute(boolean, int)}. 324 */ 325 @AddedIn(PlatformVersion.TIRAMISU_0) setMasterMute(@onNull AudioManager audioManager, boolean mute, int flags)326 public static void setMasterMute(@NonNull AudioManager audioManager, boolean mute, int flags) { 327 Objects.requireNonNull(audioManager, "AudioManager must not be null."); 328 audioManager.setMasterMute(mute, flags); 329 } 330 331 /** 332 * Gets system master mute state. 333 * 334 * <p>See {@link android.media.AudioManager#isMasterMute()}. 335 */ 336 @AddedIn(PlatformVersion.TIRAMISU_0) isMasterMute(@onNull AudioManager audioManager)337 public static boolean isMasterMute(@NonNull AudioManager audioManager) { 338 Objects.requireNonNull(audioManager, "AudioManager must not be null."); 339 return audioManager.isMasterMute(); 340 } 341 342 /** 343 * Registers volume and mute receiver 344 */ 345 @AddedIn(PlatformVersion.TIRAMISU_0) registerVolumeAndMuteReceiver(Context context, VolumeAndMuteReceiver audioAndMuteHelper)346 public static void registerVolumeAndMuteReceiver(Context context, 347 VolumeAndMuteReceiver audioAndMuteHelper) { 348 Objects.requireNonNull(context, "Context can not be null."); 349 Objects.requireNonNull(audioAndMuteHelper, "Audio and Mute helper can not be null."); 350 351 IntentFilter intentFilter = new IntentFilter(); 352 intentFilter.addAction(VOLUME_CHANGED_ACTION); 353 intentFilter.addAction(MASTER_MUTE_CHANGED_ACTION); 354 context.registerReceiver(audioAndMuteHelper.getReceiver(), intentFilter, 355 Context.RECEIVER_NOT_EXPORTED); 356 } 357 358 /** 359 * Unregisters volume and mute receiver 360 */ 361 @AddedIn(PlatformVersion.TIRAMISU_0) unregisterVolumeAndMuteReceiver(Context context, VolumeAndMuteReceiver audioAndMuteHelper)362 public static void unregisterVolumeAndMuteReceiver(Context context, 363 VolumeAndMuteReceiver audioAndMuteHelper) { 364 Objects.requireNonNull(context, "Context can not be null."); 365 Objects.requireNonNull(audioAndMuteHelper, "Audio and Mute helper can not be null."); 366 367 context.unregisterReceiver(audioAndMuteHelper.getReceiver()); 368 } 369 370 /** 371 * Checks if the client id is equal to the telephony's focus client id. 372 */ 373 @AddedIn(PlatformVersion.TIRAMISU_0) isCallFocusRequestClientId(String clientId)374 public static boolean isCallFocusRequestClientId(String clientId) { 375 return AudioSystem.IN_VOICE_COMM_FOCUS_ID.equals(clientId); 376 } 377 378 379 /** 380 * Audio gain information for a particular device: 381 * Contains Max, Min, Default gain and the step value between gain changes 382 */ 383 public static class AudioGainInfo { 384 385 private final int mMinGain; 386 private final int mMaxGain; 387 private final int mDefaultGain; 388 private final int mStepValue; 389 AudioGainInfo(AudioGain gain)390 private AudioGainInfo(AudioGain gain) { 391 mMinGain = gain.minValue(); 392 mMaxGain = gain.maxValue(); 393 mDefaultGain = gain.defaultValue(); 394 mStepValue = gain.stepValue(); 395 } 396 397 @AddedIn(PlatformVersion.TIRAMISU_0) getMinGain()398 public int getMinGain() { 399 return mMinGain; 400 } 401 402 @AddedIn(PlatformVersion.TIRAMISU_0) getMaxGain()403 public int getMaxGain() { 404 return mMaxGain; 405 } 406 407 @AddedIn(PlatformVersion.TIRAMISU_0) getDefaultGain()408 public int getDefaultGain() { 409 return mDefaultGain; 410 } 411 412 @AddedIn(PlatformVersion.TIRAMISU_0) getStepValue()413 public int getStepValue() { 414 return mStepValue; 415 } 416 } 417 418 /** 419 * Contains the audio patch information for the created audio patch: 420 * Patch handle id, source device address, sink device address 421 */ 422 public static class AudioPatchInfo { 423 private final int mHandleId; 424 425 private final String mSourceAddress; 426 private final String mSinkAddress; 427 428 AudioPatchInfo(@onNull String sourceAddress, @NonNull String sinkAddress, int handleId)429 public AudioPatchInfo(@NonNull String sourceAddress, @NonNull String sinkAddress, 430 int handleId) { 431 mSourceAddress = Preconditions.checkNotNull(sourceAddress, 432 "Source Address can not be null for patch id %d", handleId); 433 mSinkAddress = Preconditions.checkNotNull(sinkAddress, 434 "Sink Address can not be null for patch id %d", handleId); 435 mHandleId = handleId; 436 } 437 438 @AddedIn(PlatformVersion.TIRAMISU_0) getHandleId()439 public int getHandleId() { 440 return mHandleId; 441 } 442 443 @AddedIn(PlatformVersion.TIRAMISU_0) getSourceAddress()444 public String getSourceAddress() { 445 return mSourceAddress; 446 } 447 448 @AddedIn(PlatformVersion.TIRAMISU_0) getSinkAddress()449 public String getSinkAddress() { 450 return mSinkAddress; 451 } 452 453 @Override 454 @AddedIn(PlatformVersion.TIRAMISU_0) toString()455 public String toString() { 456 StringBuilder builder = new StringBuilder(); 457 builder.append("Source{ "); 458 builder.append(mSourceAddress); 459 builder.append("} Sink{ "); 460 builder.append(mSinkAddress); 461 builder.append("} Handle{ "); 462 builder.append(mHandleId); 463 builder.append("}"); 464 return builder.toString(); 465 } 466 represents(AudioPatch patch)467 private boolean represents(AudioPatch patch) { 468 return patch.id() == mHandleId; 469 } 470 } 471 472 /** 473 * Class to manage volume and mute changes from audio manager 474 */ 475 public abstract static class VolumeAndMuteReceiver { 476 477 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 478 479 @Override 480 public void onReceive(Context context, Intent intent) { 481 switch (intent.getAction()) { 482 case VOLUME_CHANGED_ACTION: 483 int streamType = 484 intent.getIntExtra(EXTRA_VOLUME_STREAM_TYPE, UNDEFINED_STREAM_TYPE); 485 onVolumeChanged(streamType); 486 break; 487 case MASTER_MUTE_CHANGED_ACTION: 488 onMuteChanged(); 489 break; 490 } 491 } 492 }; 493 getReceiver()494 private BroadcastReceiver getReceiver() { 495 return mReceiver; 496 } 497 498 /** 499 * Called on volume changes 500 * @param streamType type of stream for the volume change 501 */ 502 @AddedIn(PlatformVersion.TIRAMISU_0) onVolumeChanged(int streamType)503 public abstract void onVolumeChanged(int streamType); 504 505 /** 506 * Called on mute changes 507 */ 508 @AddedIn(PlatformVersion.TIRAMISU_0) onMuteChanged()509 public abstract void onMuteChanged(); 510 } 511 } 512