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