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