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