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}