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