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