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