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