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.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}