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