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