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