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