AppRTCAudioManager.java

  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}