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}