1/*
2 * Copyright 2014 The WebRTC Project Authors. All rights reserved.
3 *
4 * Use of this source code is governed by a BSD-style license
5 * that can be found in the LICENSE file in the root of the source
6 * tree. An additional intellectual property rights grant can be found
7 * in the file PATENTS. All contributing project authors may
8 * be found in the AUTHORS file in the root of the source tree.
9 */
10package eu.siacs.conversations.services;
11
12import android.content.BroadcastReceiver;
13import android.content.Context;
14import android.content.Intent;
15import android.content.IntentFilter;
16import android.content.pm.PackageManager;
17import android.media.AudioDeviceInfo;
18import android.media.AudioFormat;
19import android.media.AudioManager;
20import android.media.AudioRecord;
21import android.media.MediaRecorder;
22import android.os.Build;
23import android.util.Log;
24
25import androidx.annotation.Nullable;
26
27import org.webrtc.ThreadUtils;
28
29import java.util.Collections;
30import java.util.HashSet;
31import java.util.Set;
32import java.util.concurrent.CountDownLatch;
33
34import eu.siacs.conversations.Config;
35import eu.siacs.conversations.utils.AppRTCUtils;
36import eu.siacs.conversations.xmpp.jingle.Media;
37
38/**
39 * AppRTCAudioManager manages all audio related parts of the AppRTC demo.
40 */
41public class AppRTCAudioManager {
42
43 private static CountDownLatch microphoneLatch;
44
45 private final Context apprtcContext;
46 // Contains speakerphone setting: auto, true or false
47 @Nullable
48 private SpeakerPhonePreference speakerPhonePreference;
49 // Handles all tasks related to Bluetooth headset devices.
50 private final AppRTCBluetoothManager bluetoothManager;
51 @Nullable
52 private final AudioManager audioManager;
53 @Nullable
54 private AudioManagerEvents audioManagerEvents;
55 private AudioManagerState amState;
56 private boolean savedIsSpeakerPhoneOn;
57 private boolean savedIsMicrophoneMute;
58 private boolean hasWiredHeadset;
59 // Default audio device; speaker phone for video calls or earpiece for audio
60 // only calls.
61 private AudioDevice defaultAudioDevice;
62 // Contains the currently selected audio device.
63 // This device is changed automatically using a certain scheme where e.g.
64 // a wired headset "wins" over speaker phone. It is also possible for a
65 // user to explicitly select a device (and overrid any predefined scheme).
66 // See |userSelectedAudioDevice| for details.
67 private AudioDevice selectedAudioDevice;
68 // Contains the user-selected audio device which overrides the predefined
69 // selection scheme.
70 // TODO(henrika): always set to AudioDevice.NONE today. Add support for
71 // explicit selection based on choice by userSelectedAudioDevice.
72 private AudioDevice userSelectedAudioDevice;
73 // Proximity sensor object. It measures the proximity of an object in cm
74 // relative to the view screen of a device and can therefore be used to
75 // assist device switching (close to ear <=> use headset earpiece if
76 // available, far from ear <=> use speaker phone).
77 @Nullable
78 private AppRTCProximitySensor proximitySensor;
79 // Contains a list of available audio devices. A Set collection is used to
80 // avoid duplicate elements.
81 private Set<AudioDevice> audioDevices = new HashSet<>();
82 // Broadcast receiver for wired headset intent broadcasts.
83 private final BroadcastReceiver wiredHeadsetReceiver;
84 // Callback method for changes in audio focus.
85 @Nullable
86 private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
87
88 private AppRTCAudioManager(Context context, final SpeakerPhonePreference speakerPhonePreference) {
89 Log.d(Config.LOGTAG, "ctor");
90 ThreadUtils.checkIsOnMainThread();
91 apprtcContext = context;
92 audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE));
93 bluetoothManager = AppRTCBluetoothManager.create(context, this);
94 wiredHeadsetReceiver = new WiredHeadsetReceiver();
95 amState = AudioManagerState.UNINITIALIZED;
96 this.speakerPhonePreference = speakerPhonePreference;
97 if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE && hasEarpiece()) {
98 defaultAudioDevice = AudioDevice.EARPIECE;
99 } else {
100 defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
101 }
102 // Create and initialize the proximity sensor.
103 // Tablet devices (e.g. Nexus 7) does not support proximity sensors.
104 // Note that, the sensor will not be active until start() has been called.
105 proximitySensor = AppRTCProximitySensor.create(context,
106 // This method will be called each time a state change is detected.
107 // Example: user holds his hand over the device (closer than ~5 cm),
108 // or removes his hand from the device.
109 this::onProximitySensorChangedState);
110 Log.d(Config.LOGTAG, "defaultAudioDevice: " + defaultAudioDevice);
111 AppRTCUtils.logDeviceInfo(Config.LOGTAG);
112 }
113
114 public void switchSpeakerPhonePreference(final SpeakerPhonePreference speakerPhonePreference) {
115 this.speakerPhonePreference = speakerPhonePreference;
116 if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE && hasEarpiece()) {
117 defaultAudioDevice = AudioDevice.EARPIECE;
118 } else {
119 defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
120 }
121 updateAudioDeviceState();
122 }
123
124 /**
125 * Construction.
126 */
127 public static AppRTCAudioManager create(Context context, SpeakerPhonePreference speakerPhonePreference) {
128 return new AppRTCAudioManager(context, speakerPhonePreference);
129 }
130
131 public static boolean isMicrophoneAvailable() {
132 microphoneLatch = new CountDownLatch(1);
133 AudioRecord audioRecord = null;
134 boolean available = true;
135 try {
136 final int sampleRate = 44100;
137 final int channel = AudioFormat.CHANNEL_IN_MONO;
138 final int format = AudioFormat.ENCODING_PCM_16BIT;
139 final int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channel, format);
140 audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, channel, format, bufferSize);
141 audioRecord.startRecording();
142 final short[] buffer = new short[bufferSize];
143 final int audioStatus = audioRecord.read(buffer, 0, bufferSize);
144 if (audioStatus == AudioRecord.ERROR_INVALID_OPERATION || audioStatus == AudioRecord.STATE_UNINITIALIZED)
145 available = false;
146 } catch (Exception e) {
147 available = false;
148 } finally {
149 release(audioRecord);
150
151 }
152 microphoneLatch.countDown();
153 return available;
154 }
155
156 private static void release(final AudioRecord audioRecord) {
157 if (audioRecord == null) {
158 return;
159 }
160 try {
161 audioRecord.release();
162 } catch (Exception e) {
163 //ignore
164 }
165 }
166
167 /**
168 * This method is called when the proximity sensor reports a state change,
169 * e.g. from "NEAR to FAR" or from "FAR to NEAR".
170 */
171 private void onProximitySensorChangedState() {
172 if (speakerPhonePreference != SpeakerPhonePreference.AUTO) {
173 return;
174 }
175 // The proximity sensor should only be activated when there are exactly two
176 // available audio devices.
177 if (audioDevices.size() == 2 && audioDevices.contains(AppRTCAudioManager.AudioDevice.EARPIECE)
178 && audioDevices.contains(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE)) {
179 if (proximitySensor.sensorReportsNearState()) {
180 // Sensor reports that a "handset is being held up to a person's ear",
181 // or "something is covering the light sensor".
182 setAudioDeviceInternal(AppRTCAudioManager.AudioDevice.EARPIECE);
183 } else {
184 // Sensor reports that a "handset is removed from a person's ear", or
185 // "the light sensor is no longer covered".
186 setAudioDeviceInternal(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
187 }
188 }
189 }
190
191 @SuppressWarnings("deprecation")
192 public void start(AudioManagerEvents audioManagerEvents) {
193 Log.d(Config.LOGTAG, AppRTCAudioManager.class.getName() + ".start()");
194 ThreadUtils.checkIsOnMainThread();
195 if (amState == AudioManagerState.RUNNING) {
196 Log.e(Config.LOGTAG, "AudioManager is already active");
197 return;
198 }
199 awaitMicrophoneLatch();
200 this.audioManagerEvents = audioManagerEvents;
201 amState = AudioManagerState.RUNNING;
202 // Store current audio state so we can restore it when stop() is called.
203 savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn();
204 savedIsMicrophoneMute = audioManager.isMicrophoneMute();
205 hasWiredHeadset = hasWiredHeadset();
206 // Create an AudioManager.OnAudioFocusChangeListener instance.
207 audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
208 // Called on the listener to notify if the audio focus for this listener has been changed.
209 // The |focusChange| value indicates whether the focus was gained, whether the focus was lost,
210 // and whether that loss is transient, or whether the new focus holder will hold it for an
211 // unknown amount of time.
212 // TODO(henrika): possibly extend support of handling audio-focus changes. Only contains
213 // logging for now.
214 @Override
215 public void onAudioFocusChange(int focusChange) {
216 final String typeOfChange;
217 switch (focusChange) {
218 case AudioManager.AUDIOFOCUS_GAIN:
219 typeOfChange = "AUDIOFOCUS_GAIN";
220 break;
221 case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
222 typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT";
223 break;
224 case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE:
225 typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE";
226 break;
227 case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:
228 typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK";
229 break;
230 case AudioManager.AUDIOFOCUS_LOSS:
231 typeOfChange = "AUDIOFOCUS_LOSS";
232 break;
233 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
234 typeOfChange = "AUDIOFOCUS_LOSS_TRANSIENT";
235 break;
236 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
237 typeOfChange = "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK";
238 break;
239 default:
240 typeOfChange = "AUDIOFOCUS_INVALID";
241 break;
242 }
243 Log.d(Config.LOGTAG, "onAudioFocusChange: " + typeOfChange);
244 }
245 };
246 // Request audio playout focus (without ducking) and install listener for changes in focus.
247 int result = audioManager.requestAudioFocus(audioFocusChangeListener,
248 AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
249 if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
250 Log.d(Config.LOGTAG, "Audio focus request granted for VOICE_CALL streams");
251 } else {
252 Log.e(Config.LOGTAG, "Audio focus request failed");
253 }
254 // Start by setting MODE_IN_COMMUNICATION as default audio mode. It is
255 // required to be in this mode when playout and/or recording starts for
256 // best possible VoIP performance.
257 audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
258 // Always disable microphone mute during a WebRTC call.
259 setMicrophoneMute(false);
260 // Set initial device states.
261 userSelectedAudioDevice = AudioDevice.NONE;
262 selectedAudioDevice = AudioDevice.NONE;
263 audioDevices.clear();
264 // Initialize and start Bluetooth if a BT device is available or initiate
265 // detection of new (enabled) BT devices.
266 bluetoothManager.start();
267 // Do initial selection of audio device. This setting can later be changed
268 // either by adding/removing a BT or wired headset or by covering/uncovering
269 // the proximity sensor.
270 updateAudioDeviceState();
271 // Register receiver for broadcast intents related to adding/removing a
272 // wired headset.
273 registerReceiver(wiredHeadsetReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG));
274 Log.d(Config.LOGTAG, "AudioManager started");
275 }
276
277 private void awaitMicrophoneLatch() {
278 final CountDownLatch latch = microphoneLatch;
279 if (latch == null) {
280 return;
281 }
282 try {
283 latch.await();
284 } catch (InterruptedException e) {
285 //ignore
286 }
287 }
288
289 @SuppressWarnings("deprecation")
290 public void stop() {
291 Log.d(Config.LOGTAG, AppRTCAudioManager.class.getName() + ".stop()");
292 ThreadUtils.checkIsOnMainThread();
293 if (amState != AudioManagerState.RUNNING) {
294 Log.e(Config.LOGTAG, "Trying to stop AudioManager in incorrect state: " + amState);
295 return;
296 }
297 amState = AudioManagerState.UNINITIALIZED;
298 unregisterReceiver(wiredHeadsetReceiver);
299 bluetoothManager.stop();
300 // Restore previously stored audio states.
301 setSpeakerphoneOn(savedIsSpeakerPhoneOn);
302 setMicrophoneMute(savedIsMicrophoneMute);
303 try {
304 audioManager.setMode(AudioManager.MODE_NORMAL);
305 } catch (final SecurityException e) {
306 Log.e(Config.LOGTAG, "Could not set mode on audio manager: " + audioManager);
307 }
308 // Abandon audio focus. Gives the previous focus owner, if any, focus.
309 audioManager.abandonAudioFocus(audioFocusChangeListener);
310 audioFocusChangeListener = null;
311 Log.d(Config.LOGTAG, "Abandoned audio focus for VOICE_CALL streams");
312 if (proximitySensor != null) {
313 proximitySensor.stop();
314 proximitySensor = null;
315 }
316 audioManagerEvents = null;
317 }
318
319 /**
320 * Changes selection of the currently active audio device.
321 */
322 private void setAudioDeviceInternal(AudioDevice device) {
323 Log.d(Config.LOGTAG, "setAudioDeviceInternal(device=" + device + ")");
324 AppRTCUtils.assertIsTrue(audioDevices.contains(device));
325 switch (device) {
326 case SPEAKER_PHONE:
327 setSpeakerphoneOn(true);
328 break;
329 case EARPIECE:
330 case WIRED_HEADSET:
331 case BLUETOOTH:
332 setSpeakerphoneOn(false);
333 break;
334 default:
335 Log.e(Config.LOGTAG, "Invalid audio device selection");
336 break;
337 }
338 selectedAudioDevice = device;
339 }
340
341 /**
342 * Changes default audio device.
343 * TODO(henrika): add usage of this method in the AppRTCMobile client.
344 */
345 public void setDefaultAudioDevice(AudioDevice defaultDevice) {
346 ThreadUtils.checkIsOnMainThread();
347 switch (defaultDevice) {
348 case SPEAKER_PHONE:
349 defaultAudioDevice = defaultDevice;
350 break;
351 case EARPIECE:
352 if (hasEarpiece()) {
353 defaultAudioDevice = defaultDevice;
354 } else {
355 defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
356 }
357 break;
358 default:
359 Log.e(Config.LOGTAG, "Invalid default audio device selection");
360 break;
361 }
362 Log.d(Config.LOGTAG, "setDefaultAudioDevice(device=" + defaultAudioDevice + ")");
363 updateAudioDeviceState();
364 }
365
366 /**
367 * Changes selection of the currently active audio device.
368 */
369 public void selectAudioDevice(AudioDevice device) {
370 ThreadUtils.checkIsOnMainThread();
371 if (!audioDevices.contains(device)) {
372 Log.e(Config.LOGTAG, "Can not select " + device + " from available " + audioDevices);
373 }
374 userSelectedAudioDevice = device;
375 updateAudioDeviceState();
376 }
377
378 /**
379 * Returns current set of available/selectable audio devices.
380 */
381 public Set<AudioDevice> getAudioDevices() {
382 ThreadUtils.checkIsOnMainThread();
383 return Collections.unmodifiableSet(new HashSet<>(audioDevices));
384 }
385
386 /**
387 * Returns the currently selected audio device.
388 */
389 public AudioDevice getSelectedAudioDevice() {
390 ThreadUtils.checkIsOnMainThread();
391 return selectedAudioDevice;
392 }
393
394 /**
395 * Helper method for receiver registration.
396 */
397 private void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
398 apprtcContext.registerReceiver(receiver, filter);
399 }
400
401 /**
402 * Helper method for unregistration of an existing receiver.
403 */
404 private void unregisterReceiver(BroadcastReceiver receiver) {
405 apprtcContext.unregisterReceiver(receiver);
406 }
407
408 /**
409 * Sets the speaker phone mode.
410 */
411 private void setSpeakerphoneOn(boolean on) {
412 boolean wasOn = audioManager.isSpeakerphoneOn();
413 if (wasOn == on) {
414 return;
415 }
416 audioManager.setSpeakerphoneOn(on);
417 }
418
419 /**
420 * Sets the microphone mute state.
421 */
422 private void setMicrophoneMute(boolean on) {
423 boolean wasMuted = audioManager.isMicrophoneMute();
424 if (wasMuted == on) {
425 return;
426 }
427 audioManager.setMicrophoneMute(on);
428 }
429
430 /**
431 * Gets the current earpiece state.
432 */
433 private boolean hasEarpiece() {
434 return apprtcContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
435 }
436
437 /**
438 * Checks whether a wired headset is connected or not.
439 * This is not a valid indication that audio playback is actually over
440 * the wired headset as audio routing depends on other conditions. We
441 * only use it as an early indicator (during initialization) of an attached
442 * wired headset.
443 */
444 @Deprecated
445 private boolean hasWiredHeadset() {
446 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
447 return audioManager.isWiredHeadsetOn();
448 } else {
449 final AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
450 for (AudioDeviceInfo device : devices) {
451 final int type = device.getType();
452 if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
453 Log.d(Config.LOGTAG, "hasWiredHeadset: found wired headset");
454 return true;
455 } else if (type == AudioDeviceInfo.TYPE_USB_DEVICE) {
456 Log.d(Config.LOGTAG, "hasWiredHeadset: found USB audio device");
457 return true;
458 }
459 }
460 return false;
461 }
462 }
463
464 /**
465 * Updates list of possible audio devices and make new device selection.
466 * TODO(henrika): add unit test to verify all state transitions.
467 */
468 public void updateAudioDeviceState() {
469 ThreadUtils.checkIsOnMainThread();
470 Log.d(Config.LOGTAG, "--- updateAudioDeviceState: "
471 + "wired headset=" + hasWiredHeadset + ", "
472 + "BT state=" + bluetoothManager.getState());
473 Log.d(Config.LOGTAG, "Device status: "
474 + "available=" + audioDevices + ", "
475 + "selected=" + selectedAudioDevice + ", "
476 + "user selected=" + userSelectedAudioDevice);
477 // Check if any Bluetooth headset is connected. The internal BT state will
478 // change accordingly.
479 // TODO(henrika): perhaps wrap required state into BT manager.
480 if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
481 || bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_UNAVAILABLE
482 || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_DISCONNECTING) {
483 bluetoothManager.updateDevice();
484 }
485 // Update the set of available audio devices.
486 Set<AudioDevice> newAudioDevices = new HashSet<>();
487 if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
488 || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
489 || bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE) {
490 newAudioDevices.add(AudioDevice.BLUETOOTH);
491 }
492 if (hasWiredHeadset) {
493 // If a wired headset is connected, then it is the only possible option.
494 newAudioDevices.add(AudioDevice.WIRED_HEADSET);
495 } else {
496 // No wired headset, hence the audio-device list can contain speaker
497 // phone (on a tablet), or speaker phone and earpiece (on mobile phone).
498 newAudioDevices.add(AudioDevice.SPEAKER_PHONE);
499 if (hasEarpiece()) {
500 newAudioDevices.add(AudioDevice.EARPIECE);
501 }
502 }
503 // Store state which is set to true if the device list has changed.
504 boolean audioDeviceSetUpdated = !audioDevices.equals(newAudioDevices);
505 // Update the existing audio device set.
506 audioDevices = newAudioDevices;
507 // Correct user selected audio devices if needed.
508 if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_UNAVAILABLE
509 && userSelectedAudioDevice == AudioDevice.BLUETOOTH) {
510 // If BT is not available, it can't be the user selection.
511 userSelectedAudioDevice = AudioDevice.NONE;
512 }
513 if (hasWiredHeadset && userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE) {
514 // If user selected speaker phone, but then plugged wired headset then make
515 // wired headset as user selected device.
516 userSelectedAudioDevice = AudioDevice.WIRED_HEADSET;
517 }
518 if (!hasWiredHeadset && userSelectedAudioDevice == AudioDevice.WIRED_HEADSET) {
519 // If user selected wired headset, but then unplugged wired headset then make
520 // speaker phone as user selected device.
521 userSelectedAudioDevice = AudioDevice.SPEAKER_PHONE;
522 }
523 // Need to start Bluetooth if it is available and user either selected it explicitly or
524 // user did not select any output device.
525 boolean needBluetoothAudioStart =
526 bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
527 && (userSelectedAudioDevice == AudioDevice.NONE
528 || userSelectedAudioDevice == AudioDevice.BLUETOOTH);
529 // Need to stop Bluetooth audio if user selected different device and
530 // Bluetooth SCO connection is established or in the process.
531 boolean needBluetoothAudioStop =
532 (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED
533 || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING)
534 && (userSelectedAudioDevice != AudioDevice.NONE
535 && userSelectedAudioDevice != AudioDevice.BLUETOOTH);
536 if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE
537 || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING
538 || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) {
539 Log.d(Config.LOGTAG, "Need BT audio: start=" + needBluetoothAudioStart + ", "
540 + "stop=" + needBluetoothAudioStop + ", "
541 + "BT state=" + bluetoothManager.getState());
542 }
543 // Start or stop Bluetooth SCO connection given states set earlier.
544 if (needBluetoothAudioStop) {
545 bluetoothManager.stopScoAudio();
546 bluetoothManager.updateDevice();
547 }
548 if (needBluetoothAudioStart && !needBluetoothAudioStop) {
549 // Attempt to start Bluetooth SCO audio (takes a few second to start).
550 if (!bluetoothManager.startScoAudio()) {
551 // Remove BLUETOOTH from list of available devices since SCO failed.
552 audioDevices.remove(AudioDevice.BLUETOOTH);
553 audioDeviceSetUpdated = true;
554 }
555 }
556 // Update selected audio device.
557 final AudioDevice newAudioDevice;
558 if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) {
559 // If a Bluetooth is connected, then it should be used as output audio
560 // device. Note that it is not sufficient that a headset is available;
561 // an active SCO channel must also be up and running.
562 newAudioDevice = AudioDevice.BLUETOOTH;
563 } else if (hasWiredHeadset) {
564 // If a wired headset is connected, but Bluetooth is not, then wired headset is used as
565 // audio device.
566 newAudioDevice = AudioDevice.WIRED_HEADSET;
567 } else {
568 // No wired headset and no Bluetooth, hence the audio-device list can contain speaker
569 // phone (on a tablet), or speaker phone and earpiece (on mobile phone).
570 // |defaultAudioDevice| contains either AudioDevice.SPEAKER_PHONE or AudioDevice.EARPIECE
571 // depending on the user's selection.
572 newAudioDevice = defaultAudioDevice;
573 }
574 // Switch to new device but only if there has been any changes.
575 if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) {
576 // Do the required device switch.
577 setAudioDeviceInternal(newAudioDevice);
578 Log.d(Config.LOGTAG, "New device status: "
579 + "available=" + audioDevices + ", "
580 + "selected=" + newAudioDevice);
581 if (audioManagerEvents != null) {
582 // Notify a listening client that audio device has been changed.
583 audioManagerEvents.onAudioDeviceChanged(selectedAudioDevice, audioDevices);
584 }
585 }
586 Log.d(Config.LOGTAG, "--- updateAudioDeviceState done");
587 }
588
589 /**
590 * AudioDevice is the names of possible audio devices that we currently
591 * support.
592 */
593 public enum AudioDevice {SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE}
594
595 /**
596 * AudioManager state.
597 */
598 public enum AudioManagerState {
599 UNINITIALIZED,
600 PREINITIALIZED,
601 RUNNING,
602 }
603
604 public enum SpeakerPhonePreference {
605 AUTO, EARPIECE, SPEAKER;
606
607 public static SpeakerPhonePreference of(final Set<Media> media) {
608 if (media.contains(Media.VIDEO)) {
609 return SPEAKER;
610 } else {
611 return EARPIECE;
612 }
613 }
614 }
615
616 /**
617 * Selected audio device change event.
618 */
619 public interface AudioManagerEvents {
620 // Callback fired once audio device is changed or list of available audio devices changed.
621 void onAudioDeviceChanged(
622 AudioDevice selectedAudioDevice, Set<AudioDevice> availableAudioDevices);
623 }
624
625 /* Receiver which handles changes in wired headset availability. */
626 private class WiredHeadsetReceiver extends BroadcastReceiver {
627 private static final int STATE_UNPLUGGED = 0;
628 private static final int STATE_PLUGGED = 1;
629 private static final int HAS_NO_MIC = 0;
630 private static final int HAS_MIC = 1;
631
632 @Override
633 public void onReceive(Context context, Intent intent) {
634 int state = intent.getIntExtra("state", STATE_UNPLUGGED);
635 int microphone = intent.getIntExtra("microphone", HAS_NO_MIC);
636 String name = intent.getStringExtra("name");
637 Log.d(Config.LOGTAG, "WiredHeadsetReceiver.onReceive" + AppRTCUtils.getThreadInfo() + ": "
638 + "a=" + intent.getAction() + ", s="
639 + (state == STATE_UNPLUGGED ? "unplugged" : "plugged") + ", m="
640 + (microphone == HAS_MIC ? "mic" : "no mic") + ", n=" + name + ", sb="
641 + isInitialStickyBroadcast());
642 hasWiredHeadset = (state == STATE_PLUGGED);
643 updateAudioDeviceState();
644 }
645 }
646}