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