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