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