1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.LOLLIPOP; 4 import static android.os.Build.VERSION_CODES.M; 5 import static android.os.Build.VERSION_CODES.N; 6 import static android.os.Build.VERSION_CODES.O; 7 import static android.os.Build.VERSION_CODES.P; 8 import static android.os.Build.VERSION_CODES.Q; 9 import static android.os.Build.VERSION_CODES.R; 10 import static android.os.Build.VERSION_CODES.S; 11 import static org.robolectric.util.ReflectionHelpers.ClassParameter.from; 12 13 import android.annotation.NonNull; 14 import android.annotation.RequiresPermission; 15 import android.annotation.TargetApi; 16 import android.media.AudioAttributes; 17 import android.media.AudioDeviceCallback; 18 import android.media.AudioDeviceInfo; 19 import android.media.AudioFormat; 20 import android.media.AudioManager; 21 import android.media.AudioPlaybackConfiguration; 22 import android.media.AudioRecordingConfiguration; 23 import android.media.IPlayer; 24 import android.media.PlayerBase; 25 import android.media.audiopolicy.AudioPolicy; 26 import android.os.Build.VERSION_CODES; 27 import android.os.Handler; 28 import android.os.Parcel; 29 import com.android.internal.util.Preconditions; 30 import com.google.common.collect.ImmutableList; 31 import java.util.ArrayList; 32 import java.util.Collections; 33 import java.util.HashMap; 34 import java.util.HashSet; 35 import java.util.List; 36 import java.util.Map; 37 import org.robolectric.RuntimeEnvironment; 38 import org.robolectric.annotation.HiddenApi; 39 import org.robolectric.annotation.Implementation; 40 import org.robolectric.annotation.Implements; 41 import org.robolectric.util.ReflectionHelpers; 42 43 @SuppressWarnings({"UnusedDeclaration"}) 44 @Implements(value = AudioManager.class, looseSignatures = true) 45 public class ShadowAudioManager { 46 47 public static final int MAX_VOLUME_MUSIC_DTMF = 15; 48 public static final int DEFAULT_MAX_VOLUME = 7; 49 public static final int MIN_VOLUME = 0; 50 public static final int DEFAULT_VOLUME = 7; 51 public static final int INVALID_VOLUME = 0; 52 public static final int FLAG_NO_ACTION = 0; 53 public static final ImmutableList<Integer> ALL_STREAMS = 54 ImmutableList.of( 55 AudioManager.STREAM_MUSIC, 56 AudioManager.STREAM_ALARM, 57 AudioManager.STREAM_NOTIFICATION, 58 AudioManager.STREAM_RING, 59 AudioManager.STREAM_SYSTEM, 60 AudioManager.STREAM_VOICE_CALL, 61 AudioManager.STREAM_DTMF); 62 63 private static final int INVALID_PATCH_HANDLE = -1; 64 private static final float MAX_VOLUME_DB = 0; 65 private static final float MIN_VOLUME_DB = -100; 66 67 private AudioFocusRequest lastAudioFocusRequest; 68 private int nextResponseValue = AudioManager.AUDIOFOCUS_REQUEST_GRANTED; 69 private AudioManager.OnAudioFocusChangeListener lastAbandonedAudioFocusListener; 70 private android.media.AudioFocusRequest lastAbandonedAudioFocusRequest; 71 private HashMap<Integer, AudioStream> streamStatus = new HashMap<>(); 72 private List<AudioPlaybackConfiguration> activePlaybackConfigurations = Collections.emptyList(); 73 private List<AudioRecordingConfiguration> activeRecordingConfigurations = ImmutableList.of(); 74 private final HashSet<AudioManager.AudioRecordingCallback> audioRecordingCallbacks = 75 new HashSet<>(); 76 private final HashSet<AudioManager.AudioPlaybackCallback> audioPlaybackCallbacks = 77 new HashSet<>(); 78 private final HashSet<AudioDeviceCallback> audioDeviceCallbacks = new HashSet<>(); 79 private int ringerMode = AudioManager.RINGER_MODE_NORMAL; 80 private int mode = AudioManager.MODE_NORMAL; 81 private boolean bluetoothA2dpOn; 82 private boolean isBluetoothScoOn; 83 private boolean isSpeakerphoneOn; 84 private boolean isMicrophoneMuted = false; 85 private boolean isMusicActive; 86 private boolean wiredHeadsetOn; 87 private boolean isBluetoothScoAvailableOffCall = false; 88 private final Map<String, String> parameters = new HashMap<>(); 89 private final Map<Integer, Boolean> streamsMuteState = new HashMap<>(); 90 private final Map<String, AudioPolicy> registeredAudioPolicies = new HashMap<>(); 91 private int audioSessionIdCounter = 1; 92 private final Map<AudioAttributes, ImmutableList<Object>> devicesForAttributes = new HashMap<>(); 93 private ImmutableList<Object> defaultDevicesForAttributes = ImmutableList.of(); 94 private List<AudioDeviceInfo> inputDevices = new ArrayList<>(); 95 private List<AudioDeviceInfo> outputDevices = new ArrayList<>(); 96 private AudioDeviceInfo communicationDevice = null; 97 ShadowAudioManager()98 public ShadowAudioManager() { 99 for (int stream : ALL_STREAMS) { 100 streamStatus.put(stream, new AudioStream(DEFAULT_VOLUME, DEFAULT_MAX_VOLUME, FLAG_NO_ACTION)); 101 } 102 streamStatus.get(AudioManager.STREAM_MUSIC).setMaxVolume(MAX_VOLUME_MUSIC_DTMF); 103 streamStatus.get(AudioManager.STREAM_DTMF).setMaxVolume(MAX_VOLUME_MUSIC_DTMF); 104 } 105 106 @Implementation getStreamMaxVolume(int streamType)107 protected int getStreamMaxVolume(int streamType) { 108 AudioStream stream = streamStatus.get(streamType); 109 return (stream != null) ? stream.getMaxVolume() : INVALID_VOLUME; 110 } 111 112 @Implementation getStreamVolume(int streamType)113 protected int getStreamVolume(int streamType) { 114 AudioStream stream = streamStatus.get(streamType); 115 return (stream != null) ? stream.getCurrentVolume() : INVALID_VOLUME; 116 } 117 118 @Implementation(minSdk = P) getStreamVolumeDb(int streamType, int index, int deviceType)119 protected float getStreamVolumeDb(int streamType, int index, int deviceType) { 120 AudioStream stream = streamStatus.get(streamType); 121 if (stream == null) { 122 return INVALID_VOLUME; 123 } 124 if (index < MIN_VOLUME || index > stream.getMaxVolume()) { 125 throw new IllegalArgumentException("Invalid stream volume index " + index); 126 } 127 if (index == MIN_VOLUME) { 128 return Float.NEGATIVE_INFINITY; 129 } 130 float interpolation = (index - MIN_VOLUME) / (float) (stream.getMaxVolume() - MIN_VOLUME); 131 return MIN_VOLUME_DB + interpolation * (MAX_VOLUME_DB - MIN_VOLUME_DB); 132 } 133 134 @Implementation setStreamVolume(int streamType, int index, int flags)135 protected void setStreamVolume(int streamType, int index, int flags) { 136 AudioStream stream = streamStatus.get(streamType); 137 if (stream != null) { 138 stream.setCurrentVolume(index); 139 stream.setFlag(flags); 140 } 141 } 142 143 @Implementation isBluetoothScoAvailableOffCall()144 protected boolean isBluetoothScoAvailableOffCall() { 145 return isBluetoothScoAvailableOffCall; 146 } 147 148 @Implementation requestAudioFocus( android.media.AudioManager.OnAudioFocusChangeListener l, int streamType, int durationHint)149 protected int requestAudioFocus( 150 android.media.AudioManager.OnAudioFocusChangeListener l, int streamType, int durationHint) { 151 lastAudioFocusRequest = new AudioFocusRequest(l, streamType, durationHint); 152 return nextResponseValue; 153 } 154 155 /** 156 * Provides a mock like interface for the requestAudioFocus method by storing the request object 157 * for later inspection and returning the value specified in setNextFocusRequestResponse. 158 */ 159 @Implementation(minSdk = O) requestAudioFocus(android.media.AudioFocusRequest audioFocusRequest)160 protected int requestAudioFocus(android.media.AudioFocusRequest audioFocusRequest) { 161 lastAudioFocusRequest = new AudioFocusRequest(audioFocusRequest); 162 return nextResponseValue; 163 } 164 165 @Implementation abandonAudioFocus(AudioManager.OnAudioFocusChangeListener l)166 protected int abandonAudioFocus(AudioManager.OnAudioFocusChangeListener l) { 167 lastAbandonedAudioFocusListener = l; 168 return nextResponseValue; 169 } 170 171 /** 172 * Provides a mock like interface for the abandonAudioFocusRequest method by storing the request 173 * object for later inspection and returning the value specified in setNextFocusRequestResponse. 174 */ 175 @Implementation(minSdk = O) abandonAudioFocusRequest(android.media.AudioFocusRequest audioFocusRequest)176 protected int abandonAudioFocusRequest(android.media.AudioFocusRequest audioFocusRequest) { 177 lastAbandonedAudioFocusListener = audioFocusRequest.getOnAudioFocusChangeListener(); 178 lastAbandonedAudioFocusRequest = audioFocusRequest; 179 return nextResponseValue; 180 } 181 182 @Implementation getRingerMode()183 protected int getRingerMode() { 184 return ringerMode; 185 } 186 187 @Implementation setRingerMode(int ringerMode)188 protected void setRingerMode(int ringerMode) { 189 if (!AudioManager.isValidRingerMode(ringerMode)) { 190 return; 191 } 192 this.ringerMode = ringerMode; 193 } 194 195 @Implementation isValidRingerMode(int ringerMode)196 public static boolean isValidRingerMode(int ringerMode) { 197 return ringerMode >= 0 198 && ringerMode 199 <= (int) ReflectionHelpers.getStaticField(AudioManager.class, "RINGER_MODE_MAX"); 200 } 201 202 @Implementation setMode(int mode)203 protected void setMode(int mode) { 204 this.mode = mode; 205 } 206 207 @Implementation getMode()208 protected int getMode() { 209 return this.mode; 210 } 211 setStreamMaxVolume(int streamMaxVolume)212 public void setStreamMaxVolume(int streamMaxVolume) { 213 streamStatus.forEach((key, value) -> value.setMaxVolume(streamMaxVolume)); 214 } 215 setStreamVolume(int streamVolume)216 public void setStreamVolume(int streamVolume) { 217 streamStatus.forEach((key, value) -> value.setCurrentVolume(streamVolume)); 218 } 219 220 @Implementation setWiredHeadsetOn(boolean on)221 protected void setWiredHeadsetOn(boolean on) { 222 wiredHeadsetOn = on; 223 } 224 225 @Implementation isWiredHeadsetOn()226 protected boolean isWiredHeadsetOn() { 227 return wiredHeadsetOn; 228 } 229 230 @Implementation setBluetoothA2dpOn(boolean on)231 protected void setBluetoothA2dpOn(boolean on) { 232 bluetoothA2dpOn = on; 233 } 234 235 @Implementation isBluetoothA2dpOn()236 protected boolean isBluetoothA2dpOn() { 237 return bluetoothA2dpOn; 238 } 239 240 @Implementation setSpeakerphoneOn(boolean on)241 protected void setSpeakerphoneOn(boolean on) { 242 isSpeakerphoneOn = on; 243 } 244 245 @Implementation isSpeakerphoneOn()246 protected boolean isSpeakerphoneOn() { 247 return isSpeakerphoneOn; 248 } 249 250 @Implementation setMicrophoneMute(boolean on)251 protected void setMicrophoneMute(boolean on) { 252 isMicrophoneMuted = on; 253 } 254 255 @Implementation isMicrophoneMute()256 protected boolean isMicrophoneMute() { 257 return isMicrophoneMuted; 258 } 259 260 @Implementation isBluetoothScoOn()261 protected boolean isBluetoothScoOn() { 262 return isBluetoothScoOn; 263 } 264 265 @Implementation setBluetoothScoOn(boolean isBluetoothScoOn)266 protected void setBluetoothScoOn(boolean isBluetoothScoOn) { 267 this.isBluetoothScoOn = isBluetoothScoOn; 268 } 269 270 @Implementation isMusicActive()271 protected boolean isMusicActive() { 272 return isMusicActive; 273 } 274 275 @Implementation(minSdk = O) getActivePlaybackConfigurations()276 protected List<AudioPlaybackConfiguration> getActivePlaybackConfigurations() { 277 return new ArrayList<>(activePlaybackConfigurations); 278 } 279 280 @Implementation setParameters(String keyValuePairs)281 protected void setParameters(String keyValuePairs) { 282 if (keyValuePairs.isEmpty()) { 283 throw new IllegalArgumentException("keyValuePairs should not be empty"); 284 } 285 286 if (keyValuePairs.charAt(keyValuePairs.length() - 1) != ';') { 287 throw new IllegalArgumentException("keyValuePairs should end with a ';'"); 288 } 289 290 String[] pairs = keyValuePairs.split(";", 0); 291 292 for (String pair : pairs) { 293 if (pair.isEmpty()) { 294 continue; 295 } 296 297 String[] splittedPair = pair.split("=", 0); 298 if (splittedPair.length != 2) { 299 throw new IllegalArgumentException( 300 "keyValuePairs: each pair should be in the format of key=value;"); 301 } 302 parameters.put(splittedPair[0], splittedPair[1]); 303 } 304 } 305 306 /** 307 * The expected composition for keys is not well defined. 308 * 309 * <p>For testing purposes this method call always returns null. 310 */ 311 @Implementation getParameters(String keys)312 protected String getParameters(String keys) { 313 return null; 314 } 315 316 /** Returns a single parameter that was set via {@link #setParameters(String)}. */ getParameter(String key)317 public String getParameter(String key) { 318 return parameters.get(key); 319 } 320 321 /** 322 * Implements {@link AudioManager#adjustStreamVolume(int, int, int)}. 323 * 324 * <p>Currently supports only the directions {@link AudioManager#ADJUST_MUTE}, {@link 325 * AudioManager#ADJUST_UNMUTE}, {@link AudioManager#ADJUST_LOWER} and {@link 326 * AudioManager#ADJUST_RAISE}. 327 */ 328 @Implementation adjustStreamVolume(int streamType, int direction, int flags)329 protected void adjustStreamVolume(int streamType, int direction, int flags) { 330 int streamVolume = getStreamVolume(streamType); 331 switch (direction) { 332 case AudioManager.ADJUST_MUTE: 333 streamsMuteState.put(streamType, true); 334 break; 335 case AudioManager.ADJUST_UNMUTE: 336 streamsMuteState.put(streamType, false); 337 break; 338 case AudioManager.ADJUST_RAISE: 339 int streamMaxVolume = getStreamMaxVolume(streamType); 340 if (streamVolume == INVALID_VOLUME || streamMaxVolume == INVALID_VOLUME) { 341 return; 342 } 343 int raisedVolume = streamVolume + 1; 344 if (raisedVolume <= streamMaxVolume) { 345 setStreamVolume(raisedVolume); 346 } 347 break; 348 case AudioManager.ADJUST_LOWER: 349 if (streamVolume == INVALID_VOLUME) { 350 return; 351 } 352 int lowerVolume = streamVolume - 1; 353 if (lowerVolume >= 1) { 354 setStreamVolume(lowerVolume); 355 } 356 break; 357 default: 358 break; 359 } 360 } 361 362 @Implementation(minSdk = M) isStreamMute(int streamType)363 protected boolean isStreamMute(int streamType) { 364 if (!streamsMuteState.containsKey(streamType)) { 365 return false; 366 } 367 return streamsMuteState.get(streamType); 368 } 369 setIsBluetoothScoAvailableOffCall(boolean isBluetoothScoAvailableOffCall)370 public void setIsBluetoothScoAvailableOffCall(boolean isBluetoothScoAvailableOffCall) { 371 this.isBluetoothScoAvailableOffCall = isBluetoothScoAvailableOffCall; 372 } 373 setIsStreamMute(int streamType, boolean isMuted)374 public void setIsStreamMute(int streamType, boolean isMuted) { 375 streamsMuteState.put(streamType, isMuted); 376 } 377 378 /** 379 * Registers callback that will receive changes made to the list of active playback configurations 380 * by {@link setActivePlaybackConfigurationsFor}. 381 */ 382 @Implementation(minSdk = O) registerAudioPlaybackCallback( AudioManager.AudioPlaybackCallback cb, Handler handler)383 protected void registerAudioPlaybackCallback( 384 AudioManager.AudioPlaybackCallback cb, Handler handler) { 385 audioPlaybackCallbacks.add(cb); 386 } 387 388 /** Unregisters callback listening to changes made to list of active playback configurations. */ 389 @Implementation(minSdk = O) unregisterAudioPlaybackCallback(AudioManager.AudioPlaybackCallback cb)390 protected void unregisterAudioPlaybackCallback(AudioManager.AudioPlaybackCallback cb) { 391 audioPlaybackCallbacks.remove(cb); 392 } 393 394 /** 395 * Returns the devices associated with the given audio stream. 396 * 397 * <p>In this shadow-implementation the devices returned are either 398 * 399 * <ol> 400 * <li>devices set through {@link #setDevicesForAttributes}, or 401 * <li>devices set through {@link #setDefaultDevicesForAttributes}, or 402 * <li>an empty list. 403 * </ol> 404 */ 405 @Implementation(minSdk = R) 406 @NonNull getDevicesForAttributes(@onNull AudioAttributes attributes)407 protected List<Object> getDevicesForAttributes(@NonNull AudioAttributes attributes) { 408 ImmutableList<Object> devices = devicesForAttributes.get(attributes); 409 return devices == null ? defaultDevicesForAttributes : devices; 410 } 411 412 /** Sets the devices associated with the given audio stream. */ setDevicesForAttributes( @onNull AudioAttributes attributes, @NonNull ImmutableList<Object> devices)413 public void setDevicesForAttributes( 414 @NonNull AudioAttributes attributes, @NonNull ImmutableList<Object> devices) { 415 devicesForAttributes.put(attributes, devices); 416 } 417 418 /** Sets the devices to use as default for all audio streams. */ setDefaultDevicesForAttributes(@onNull ImmutableList<Object> devices)419 public void setDefaultDevicesForAttributes(@NonNull ImmutableList<Object> devices) { 420 defaultDevicesForAttributes = devices; 421 } 422 423 /** 424 * Sets the list of connected input devices represented by {@link AudioDeviceInfo}. 425 * 426 * <p>The previous list of input devices is replaced and no notifications of the list of {@link 427 * AudioDeviceCallback} is done. 428 * 429 * <p>To add/remove devices one by one and trigger notifications for the list of {@link 430 * AudioDeviceCallback} please use one of the following methods {@link 431 * #addInputDevice(AudioDeviceInfo, boolean)}, {@link #removeInputDevice(AudioDeviceInfo, 432 * boolean)}. 433 */ setInputDevices(List<AudioDeviceInfo> inputDevices)434 public void setInputDevices(List<AudioDeviceInfo> inputDevices) { 435 this.inputDevices = new ArrayList<>(inputDevices); 436 } 437 438 /** 439 * Sets the list of connected output devices represented by {@link AudioDeviceInfo}. 440 * 441 * <p>The previous list of output devices is replaced and no notifications of the list of {@link 442 * AudioDeviceCallback} is done. 443 * 444 * <p>To add/remove devices one by one and trigger notifications for the list of {@link 445 * AudioDeviceCallback} please use one of the following methods {@link 446 * #addOutputDevice(AudioDeviceInfo, boolean)}, {@link #removeOutputDevice(AudioDeviceInfo, 447 * boolean)}. 448 */ setOutputDevices(List<AudioDeviceInfo> outputDevices)449 public void setOutputDevices(List<AudioDeviceInfo> outputDevices) { 450 this.outputDevices = new ArrayList<>(outputDevices); 451 } 452 453 /** 454 * Adds an input {@link AudioDeviceInfo} and notifies the list of {@link AudioDeviceCallback} if 455 * the device was not present before and indicated by {@code notifyAudioDeviceCallbacks}. 456 */ addInputDevice(AudioDeviceInfo inputDevice, boolean notifyAudioDeviceCallbacks)457 public void addInputDevice(AudioDeviceInfo inputDevice, boolean notifyAudioDeviceCallbacks) { 458 boolean changed = 459 !this.inputDevices.contains(inputDevice) && this.inputDevices.add(inputDevice); 460 if (changed && notifyAudioDeviceCallbacks) { 461 notifyAudioDeviceCallbacks(ImmutableList.of(inputDevice), /* added= */ true); 462 } 463 } 464 465 /** 466 * Removes an input {@link AudioDeviceInfo} and notifies the list of {@link AudioDeviceCallback} 467 * if the device was present before and indicated by {@code notifyAudioDeviceCallbacks}. 468 */ removeInputDevice(AudioDeviceInfo inputDevice, boolean notifyAudioDeviceCallbacks)469 public void removeInputDevice(AudioDeviceInfo inputDevice, boolean notifyAudioDeviceCallbacks) { 470 boolean changed = this.inputDevices.remove(inputDevice); 471 if (changed && notifyAudioDeviceCallbacks) { 472 notifyAudioDeviceCallbacks(ImmutableList.of(inputDevice), /* added= */ false); 473 } 474 } 475 476 /** 477 * Adds an output {@link AudioDeviceInfo} and notifies the list of {@link AudioDeviceCallback} if 478 * the device was not present before and indicated by {@code notifyAudioDeviceCallbacks}. 479 */ addOutputDevice(AudioDeviceInfo outputDevice, boolean notifyAudioDeviceCallbacks)480 public void addOutputDevice(AudioDeviceInfo outputDevice, boolean notifyAudioDeviceCallbacks) { 481 boolean changed = 482 !this.outputDevices.contains(outputDevice) && this.outputDevices.add(outputDevice); 483 if (changed && notifyAudioDeviceCallbacks) { 484 notifyAudioDeviceCallbacks(ImmutableList.of(outputDevice), /* added= */ true); 485 } 486 } 487 488 /** 489 * Removes an output {@link AudioDeviceInfo} and notifies the list of {@link AudioDeviceCallback} 490 * if the device was present before and indicated by {@code notifyAudioDeviceCallbacks}. 491 */ removeOutputDevice(AudioDeviceInfo outputDevice, boolean notifyAudioDeviceCallbacks)492 public void removeOutputDevice(AudioDeviceInfo outputDevice, boolean notifyAudioDeviceCallbacks) { 493 boolean changed = this.outputDevices.remove(outputDevice); 494 if (changed && notifyAudioDeviceCallbacks) { 495 notifyAudioDeviceCallbacks(ImmutableList.of(outputDevice), /* added= */ false); 496 } 497 } 498 499 /** 500 * Registers an {@link AudioDeviceCallback} object to receive notifications of changes to the set 501 * of connected audio devices. 502 * 503 * <p>The {@code handler} is ignored. 504 * 505 * @see #addInputDevice(AudioDeviceInfo, boolean) 506 * @see #addOutputDevice(AudioDeviceInfo, boolean) 507 * @see #removeInputDevice(AudioDeviceInfo, boolean) 508 * @see #removeOutputDevice(AudioDeviceInfo, boolean) 509 */ 510 @Implementation(minSdk = M) registerAudioDeviceCallback(AudioDeviceCallback callback, Handler handler)511 protected void registerAudioDeviceCallback(AudioDeviceCallback callback, Handler handler) { 512 audioDeviceCallbacks.add(callback); 513 // indicate currently available devices as added, similarly to MSG_DEVICES_CALLBACK_REGISTERED 514 callback.onAudioDevicesAdded(getDevices(AudioManager.GET_DEVICES_ALL)); 515 } 516 517 /** 518 * Unregisters an {@link AudioDeviceCallback} object which has been previously registered to 519 * receive notifications of changes to the set of connected audio devices. 520 * 521 * @see #addInputDevice(AudioDeviceInfo, boolean) 522 * @see #addOutputDevice(AudioDeviceInfo, boolean) 523 * @see #removeInputDevice(AudioDeviceInfo, boolean) 524 * @see #removeOutputDevice(AudioDeviceInfo, boolean) 525 */ 526 @Implementation(minSdk = M) unregisterAudioDeviceCallback(AudioDeviceCallback callback)527 protected void unregisterAudioDeviceCallback(AudioDeviceCallback callback) { 528 audioDeviceCallbacks.remove(callback); 529 } 530 notifyAudioDeviceCallbacks(List<AudioDeviceInfo> devices, boolean added)531 private void notifyAudioDeviceCallbacks(List<AudioDeviceInfo> devices, boolean added) { 532 AudioDeviceInfo[] devicesArray = devices.toArray(new AudioDeviceInfo[0]); 533 for (AudioDeviceCallback callback : audioDeviceCallbacks) { 534 if (added) { 535 callback.onAudioDevicesAdded(devicesArray); 536 } else { 537 callback.onAudioDevicesRemoved(devicesArray); 538 } 539 } 540 } 541 getInputDevices()542 private List<AudioDeviceInfo> getInputDevices() { 543 return inputDevices; 544 } 545 getOutputDevices()546 private List<AudioDeviceInfo> getOutputDevices() { 547 return outputDevices; 548 } 549 550 @Implementation(minSdk = S) setCommunicationDevice(AudioDeviceInfo communicationDevice)551 protected boolean setCommunicationDevice(AudioDeviceInfo communicationDevice) { 552 this.communicationDevice = communicationDevice; 553 return true; 554 } 555 556 @Implementation(minSdk = S) getCommunicationDevice()557 protected AudioDeviceInfo getCommunicationDevice() { 558 return communicationDevice; 559 } 560 561 @Implementation(minSdk = S) clearCommunicationDevice()562 protected void clearCommunicationDevice() { 563 this.communicationDevice = null; 564 } 565 566 @Implementation(minSdk = M) getDevices(int flags)567 public AudioDeviceInfo[] getDevices(int flags) { 568 List<AudioDeviceInfo> result = new ArrayList<>(); 569 if ((flags & AudioManager.GET_DEVICES_INPUTS) == AudioManager.GET_DEVICES_INPUTS) { 570 result.addAll(getInputDevices()); 571 } 572 if ((flags & AudioManager.GET_DEVICES_OUTPUTS) == AudioManager.GET_DEVICES_OUTPUTS) { 573 result.addAll(getOutputDevices()); 574 } 575 return result.toArray(new AudioDeviceInfo[0]); 576 } 577 578 /** 579 * Sets active playback configurations that will be served by {@link 580 * AudioManager#getActivePlaybackConfigurations}. 581 * 582 * <p>Note that there is no public {@link AudioPlaybackConfiguration} constructor, so the 583 * configurations returned are specified by their audio attributes only. 584 */ 585 @TargetApi(VERSION_CODES.O) setActivePlaybackConfigurationsFor(List<AudioAttributes> audioAttributes)586 public void setActivePlaybackConfigurationsFor(List<AudioAttributes> audioAttributes) { 587 setActivePlaybackConfigurationsFor(audioAttributes, /* notifyCallbackListeners= */ false); 588 } 589 590 /** 591 * Same as {@link #setActivePlaybackConfigurationsFor(List)}, but also notifies callbacks if 592 * notifyCallbackListeners is true. 593 */ 594 @TargetApi(VERSION_CODES.O) setActivePlaybackConfigurationsFor( List<AudioAttributes> audioAttributes, boolean notifyCallbackListeners)595 public void setActivePlaybackConfigurationsFor( 596 List<AudioAttributes> audioAttributes, boolean notifyCallbackListeners) { 597 if (RuntimeEnvironment.getApiLevel() < O) { 598 throw new UnsupportedOperationException( 599 "setActivePlaybackConfigurationsFor is not supported on API " 600 + RuntimeEnvironment.getApiLevel()); 601 } 602 activePlaybackConfigurations = new ArrayList<>(audioAttributes.size()); 603 for (AudioAttributes audioAttribute : audioAttributes) { 604 AudioPlaybackConfiguration configuration = createAudioPlaybackConfiguration(audioAttribute); 605 activePlaybackConfigurations.add(configuration); 606 } 607 if (notifyCallbackListeners) { 608 for (AudioManager.AudioPlaybackCallback callback : audioPlaybackCallbacks) { 609 callback.onPlaybackConfigChanged(activePlaybackConfigurations); 610 } 611 } 612 } 613 createAudioPlaybackConfiguration( AudioAttributes audioAttributes)614 protected AudioPlaybackConfiguration createAudioPlaybackConfiguration( 615 AudioAttributes audioAttributes) { 616 // use reflection to call package private APIs 617 if (RuntimeEnvironment.getApiLevel() >= S) { 618 PlayerBase.PlayerIdCard playerIdCard = 619 ReflectionHelpers.callConstructor( 620 PlayerBase.PlayerIdCard.class, 621 ReflectionHelpers.ClassParameter.from(int.class, 0), /* type */ 622 ReflectionHelpers.ClassParameter.from(AudioAttributes.class, audioAttributes), 623 ReflectionHelpers.ClassParameter.from(IPlayer.class, null), 624 ReflectionHelpers.ClassParameter.from(int.class, 0) /* sessionId */); 625 AudioPlaybackConfiguration config = 626 ReflectionHelpers.callConstructor( 627 AudioPlaybackConfiguration.class, 628 ReflectionHelpers.ClassParameter.from(PlayerBase.PlayerIdCard.class, playerIdCard), 629 ReflectionHelpers.ClassParameter.from(int.class, 0), /* piid */ 630 ReflectionHelpers.ClassParameter.from(int.class, 0), /* uid */ 631 ReflectionHelpers.ClassParameter.from(int.class, 0) /* pid */); 632 ReflectionHelpers.setField( 633 config, "mPlayerState", AudioPlaybackConfiguration.PLAYER_STATE_STARTED); 634 return config; 635 } else { 636 PlayerBase.PlayerIdCard playerIdCard = 637 ReflectionHelpers.callConstructor( 638 PlayerBase.PlayerIdCard.class, 639 from(int.class, 0), /* type */ 640 from(AudioAttributes.class, audioAttributes), 641 from(IPlayer.class, null)); 642 AudioPlaybackConfiguration config = 643 ReflectionHelpers.callConstructor( 644 AudioPlaybackConfiguration.class, 645 from(PlayerBase.PlayerIdCard.class, playerIdCard), 646 from(int.class, 0), /* piid */ 647 from(int.class, 0), /* uid */ 648 from(int.class, 0) /* pid */); 649 ReflectionHelpers.setField( 650 config, "mPlayerState", AudioPlaybackConfiguration.PLAYER_STATE_STARTED); 651 return config; 652 } 653 } 654 setIsMusicActive(boolean isMusicActive)655 public void setIsMusicActive(boolean isMusicActive) { 656 this.isMusicActive = isMusicActive; 657 } 658 getLastAudioFocusRequest()659 public AudioFocusRequest getLastAudioFocusRequest() { 660 return lastAudioFocusRequest; 661 } 662 setNextFocusRequestResponse(int nextResponseValue)663 public void setNextFocusRequestResponse(int nextResponseValue) { 664 this.nextResponseValue = nextResponseValue; 665 } 666 getLastAbandonedAudioFocusListener()667 public AudioManager.OnAudioFocusChangeListener getLastAbandonedAudioFocusListener() { 668 return lastAbandonedAudioFocusListener; 669 } 670 getLastAbandonedAudioFocusRequest()671 public android.media.AudioFocusRequest getLastAbandonedAudioFocusRequest() { 672 return lastAbandonedAudioFocusRequest; 673 } 674 675 /** 676 * Returns list of active recording configurations that was set by {@link 677 * #setActiveRecordingConfigurations} or empty list otherwise. 678 */ 679 @Implementation(minSdk = N) getActiveRecordingConfigurations()680 protected List<AudioRecordingConfiguration> getActiveRecordingConfigurations() { 681 return activeRecordingConfigurations; 682 } 683 684 /** 685 * Registers callback that will receive changes made to the list of active recording 686 * configurations by {@link setActiveRecordingConfigurations}. 687 */ 688 @Implementation(minSdk = N) registerAudioRecordingCallback( AudioManager.AudioRecordingCallback cb, Handler handler)689 protected void registerAudioRecordingCallback( 690 AudioManager.AudioRecordingCallback cb, Handler handler) { 691 audioRecordingCallbacks.add(cb); 692 } 693 694 /** Unregisters callback listening to changes made to list of active recording configurations. */ 695 @Implementation(minSdk = N) unregisterAudioRecordingCallback(AudioManager.AudioRecordingCallback cb)696 protected void unregisterAudioRecordingCallback(AudioManager.AudioRecordingCallback cb) { 697 audioRecordingCallbacks.remove(cb); 698 } 699 700 /** 701 * Sets active recording configurations that will be served by {@link 702 * AudioManager#getActiveRecordingConfigurations} and notifies callback listeners about that 703 * change. 704 */ setActiveRecordingConfigurations( List<AudioRecordingConfiguration> activeRecordingConfigurations, boolean notifyCallbackListeners)705 public void setActiveRecordingConfigurations( 706 List<AudioRecordingConfiguration> activeRecordingConfigurations, 707 boolean notifyCallbackListeners) { 708 this.activeRecordingConfigurations = new ArrayList<>(activeRecordingConfigurations); 709 710 if (notifyCallbackListeners) { 711 for (AudioManager.AudioRecordingCallback callback : audioRecordingCallbacks) { 712 callback.onRecordingConfigChanged(this.activeRecordingConfigurations); 713 } 714 } 715 } 716 717 /** 718 * Creates simple active recording configuration. The resulting configuration will return {@code 719 * null} for {@link android.media.AudioRecordingConfiguration#getAudioDevice}. 720 */ createActiveRecordingConfiguration( int sessionId, int audioSource, String clientPackageName)721 public AudioRecordingConfiguration createActiveRecordingConfiguration( 722 int sessionId, int audioSource, String clientPackageName) { 723 Parcel p = Parcel.obtain(); 724 p.writeInt(sessionId); // mSessionId 725 p.writeInt(audioSource); // mClientSource 726 writeMono16BitAudioFormatToParcel(p); // mClientFormat 727 writeMono16BitAudioFormatToParcel(p); // mDeviceFormat 728 p.writeInt(INVALID_PATCH_HANDLE); // mPatchHandle 729 p.writeString(clientPackageName); // mClientPackageName 730 p.writeInt(0); // mClientUid 731 732 p.setDataPosition(0); 733 734 AudioRecordingConfiguration configuration = 735 AudioRecordingConfiguration.CREATOR.createFromParcel(p); 736 p.recycle(); 737 738 return configuration; 739 } 740 741 /** 742 * Registers an {@link AudioPolicy} to allow that policy to control audio routing and audio focus. 743 * 744 * <p>Note: this implementation does NOT ensure that we have the permissions necessary to register 745 * the given {@link AudioPolicy}. 746 * 747 * @return {@link AudioManager.ERROR} if the given policy has already been registered, and {@link 748 * AudioManager.SUCCESS} otherwise. 749 */ 750 @HiddenApi 751 @Implementation(minSdk = P) 752 @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) registerAudioPolicy(@onNull Object audioPolicy)753 protected int registerAudioPolicy(@NonNull Object audioPolicy) { 754 Preconditions.checkNotNull(audioPolicy, "Illegal null AudioPolicy argument"); 755 AudioPolicy policy = (AudioPolicy) audioPolicy; 756 String id = getIdForAudioPolicy(audioPolicy); 757 if (registeredAudioPolicies.containsKey(id)) { 758 return AudioManager.ERROR; 759 } 760 registeredAudioPolicies.put(id, policy); 761 policy.setRegistration(id); 762 return AudioManager.SUCCESS; 763 } 764 765 @HiddenApi 766 @Implementation(minSdk = Q) unregisterAudioPolicy(@onNull Object audioPolicy)767 protected void unregisterAudioPolicy(@NonNull Object audioPolicy) { 768 Preconditions.checkNotNull(audioPolicy, "Illegal null AudioPolicy argument"); 769 AudioPolicy policy = (AudioPolicy) audioPolicy; 770 registeredAudioPolicies.remove(getIdForAudioPolicy(policy)); 771 policy.setRegistration(null); 772 } 773 774 /** 775 * Returns true if at least one audio policy is registered with this manager, and false otherwise. 776 */ isAnyAudioPolicyRegistered()777 public boolean isAnyAudioPolicyRegistered() { 778 return !registeredAudioPolicies.isEmpty(); 779 } 780 781 /** 782 * Provides a mock like interface for the {@link AudioManager#generateAudioSessionId} method by 783 * returning positive distinct values, or {@link AudioManager#ERROR} if all possible values have 784 * already been returned. 785 */ 786 @Implementation(minSdk = LOLLIPOP) generateAudioSessionId()787 protected int generateAudioSessionId() { 788 if (audioSessionIdCounter < 0) { 789 return AudioManager.ERROR; 790 } 791 792 return audioSessionIdCounter++; 793 } 794 getIdForAudioPolicy(@onNull Object audioPolicy)795 private static String getIdForAudioPolicy(@NonNull Object audioPolicy) { 796 return Integer.toString(System.identityHashCode(audioPolicy)); 797 } 798 writeMono16BitAudioFormatToParcel(Parcel p)799 private static void writeMono16BitAudioFormatToParcel(Parcel p) { 800 p.writeInt( 801 AudioFormat.AUDIO_FORMAT_HAS_PROPERTY_ENCODING 802 + AudioFormat.AUDIO_FORMAT_HAS_PROPERTY_SAMPLE_RATE 803 + AudioFormat.AUDIO_FORMAT_HAS_PROPERTY_CHANNEL_MASK); // mPropertySetMask 804 p.writeInt(AudioFormat.ENCODING_PCM_16BIT); // mEncoding 805 p.writeInt(16000); // mSampleRate 806 p.writeInt(AudioFormat.CHANNEL_OUT_MONO); // mChannelMask 807 p.writeInt(0); // mChannelIndexMask 808 } 809 810 public static class AudioFocusRequest { 811 public final AudioManager.OnAudioFocusChangeListener listener; 812 public final int streamType; 813 public final int durationHint; 814 public final android.media.AudioFocusRequest audioFocusRequest; 815 AudioFocusRequest( AudioManager.OnAudioFocusChangeListener listener, int streamType, int durationHint)816 private AudioFocusRequest( 817 AudioManager.OnAudioFocusChangeListener listener, int streamType, int durationHint) { 818 this.listener = listener; 819 this.streamType = streamType; 820 this.durationHint = durationHint; 821 this.audioFocusRequest = null; 822 } 823 AudioFocusRequest(android.media.AudioFocusRequest audioFocusRequest)824 private AudioFocusRequest(android.media.AudioFocusRequest audioFocusRequest) { 825 this.listener = audioFocusRequest.getOnAudioFocusChangeListener(); 826 this.durationHint = audioFocusRequest.getFocusGain(); 827 this.streamType = audioFocusRequest.getAudioAttributes().getVolumeControlStream(); 828 this.audioFocusRequest = audioFocusRequest; 829 } 830 } 831 832 private static class AudioStream { 833 private int currentVolume; 834 private int maxVolume; 835 private int flag; 836 AudioStream(int currVol, int maxVol, int flag)837 public AudioStream(int currVol, int maxVol, int flag) { 838 if (MIN_VOLUME > maxVol) { 839 throw new IllegalArgumentException("Min volume is higher than max volume."); 840 } 841 setCurrentVolume(currVol); 842 setMaxVolume(maxVol); 843 setFlag(flag); 844 } 845 getCurrentVolume()846 public int getCurrentVolume() { 847 return currentVolume; 848 } 849 getMaxVolume()850 public int getMaxVolume() { 851 return maxVolume; 852 } 853 getFlag()854 public int getFlag() { 855 return flag; 856 } 857 setCurrentVolume(int vol)858 public void setCurrentVolume(int vol) { 859 if (vol > maxVolume) { 860 vol = maxVolume; 861 } else if (vol < MIN_VOLUME) { 862 vol = MIN_VOLUME; 863 } 864 currentVolume = vol; 865 } 866 setMaxVolume(int vol)867 public void setMaxVolume(int vol) { 868 maxVolume = vol; 869 } 870 setFlag(int flag)871 public void setFlag(int flag) { 872 this.flag = flag; 873 } 874 } 875 } 876